@surea11y/core 1.0.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 (164) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +145 -0
  4. package/bin/core.js +244 -0
  5. package/docs/BINDING_AUTHORS_GUIDE.md +41 -0
  6. package/docs/CLI.md +49 -0
  7. package/docs/ENGINE_OPTIONS.md +155 -0
  8. package/docs/I18N.md +47 -0
  9. package/docs/INTEGRATION.md +156 -0
  10. package/docs/LIMITATIONS.md +31 -0
  11. package/docs/OUTPUT_SCHEMA.md +237 -0
  12. package/docs/POLICY.md +71 -0
  13. package/docs/RULE_AUTHORING.md +375 -0
  14. package/docs/RULE_CATALOG.md +180 -0
  15. package/docs/RULE_TAXONOMY.md +145 -0
  16. package/docs/TROUBLESHOOTING.md +48 -0
  17. package/docs/WCAG_CONFORMANCE.md +49 -0
  18. package/package.json +60 -0
  19. package/src/catalogs/composites.wcag.js +490 -0
  20. package/src/checks/automatic/area-alt-present.js +225 -0
  21. package/src/checks/automatic/aria-allowed-attr.js +206 -0
  22. package/src/checks/automatic/aria-allowed-role.js +102 -0
  23. package/src/checks/automatic/aria-braille-equivalent.js +139 -0
  24. package/src/checks/automatic/aria-conditional-attr.js +110 -0
  25. package/src/checks/automatic/aria-deprecated-role.js +106 -0
  26. package/src/checks/automatic/aria-hidden-body.js +87 -0
  27. package/src/checks/automatic/aria-hidden-focus.js +480 -0
  28. package/src/checks/automatic/aria-prohibited-attr.js +156 -0
  29. package/src/checks/automatic/aria-prohibited-children.js +265 -0
  30. package/src/checks/automatic/aria-required-attr.js +154 -0
  31. package/src/checks/automatic/aria-required-children.js +274 -0
  32. package/src/checks/automatic/aria-required-parent.js +222 -0
  33. package/src/checks/automatic/aria-role-name-present.js +201 -0
  34. package/src/checks/automatic/aria-roles-valid.js +110 -0
  35. package/src/checks/automatic/aria-valid-attr-value.js +123 -0
  36. package/src/checks/automatic/aria-valid-attr.js +109 -0
  37. package/src/checks/automatic/autocomplete-valid.js +134 -0
  38. package/src/checks/automatic/avoid-inline-spacing.js +107 -0
  39. package/src/checks/automatic/binary-control-name-present.js +294 -0
  40. package/src/checks/automatic/button-name-present.js +146 -0
  41. package/src/checks/automatic/bypass-blocks-present.js +162 -0
  42. package/src/checks/automatic/canvas-text-alternative-present.js +140 -0
  43. package/src/checks/automatic/combobox-name-present.js +267 -0
  44. package/src/checks/automatic/contrast-computable.js +378 -0
  45. package/src/checks/automatic/contrast-enhanced.js +517 -0
  46. package/src/checks/automatic/contrast-minimum.js +512 -0
  47. package/src/checks/automatic/css-orientation-lock.js +206 -0
  48. package/src/checks/automatic/definition-list-children-valid.js +148 -0
  49. package/src/checks/automatic/deprecated-elements-not-used.js +91 -0
  50. package/src/checks/automatic/dialog-name-present.js +209 -0
  51. package/src/checks/automatic/dlitem-parent-valid.js +100 -0
  52. package/src/checks/automatic/duplicate-id-aria.js +126 -0
  53. package/src/checks/automatic/embed-text-alternative-present.js +190 -0
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +409 -0
  55. package/src/checks/automatic/form-control-single-label.js +117 -0
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +91 -0
  57. package/src/checks/automatic/iframe-focusable-content.js +141 -0
  58. package/src/checks/automatic/iframe-name-present.js +102 -0
  59. package/src/checks/automatic/iframe-title-unique.js +107 -0
  60. package/src/checks/automatic/img-alt-present.js +223 -0
  61. package/src/checks/automatic/input-image-alt-present.js +155 -0
  62. package/src/checks/automatic/label-in-name.js +326 -0
  63. package/src/checks/automatic/language-page-present.js +159 -0
  64. package/src/checks/automatic/link-in-text-block.js +218 -0
  65. package/src/checks/automatic/link-name-present.js +114 -0
  66. package/src/checks/automatic/list-children-valid.js +152 -0
  67. package/src/checks/automatic/listbox-name-present.js +236 -0
  68. package/src/checks/automatic/listitem-parent-valid.js +118 -0
  69. package/src/checks/automatic/menuitem-name-present.js +201 -0
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +105 -0
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +107 -0
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +118 -0
  73. package/src/checks/automatic/meter-name-present.js +160 -0
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +135 -0
  75. package/src/checks/automatic/object-text-alternative-present.js +193 -0
  76. package/src/checks/automatic/option-name-present.js +157 -0
  77. package/src/checks/automatic/page-title-present.js +86 -0
  78. package/src/checks/automatic/progressbar-name-present.js +165 -0
  79. package/src/checks/automatic/role-img-alt-present.js +206 -0
  80. package/src/checks/automatic/searchbox-name-present.js +236 -0
  81. package/src/checks/automatic/server-side-image-map-absent.js +88 -0
  82. package/src/checks/automatic/slider-name-present.js +276 -0
  83. package/src/checks/automatic/spinbutton-name-present.js +236 -0
  84. package/src/checks/automatic/summary-name-present.js +153 -0
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +220 -0
  86. package/src/checks/automatic/svg-text-alternative-present.js +298 -0
  87. package/src/checks/automatic/tab-name-present.js +200 -0
  88. package/src/checks/automatic/table-headers-attr-valid.js +122 -0
  89. package/src/checks/automatic/table-th-has-data-cells.js +117 -0
  90. package/src/checks/automatic/target-size-minimum.js +605 -0
  91. package/src/checks/automatic/td-has-header.js +151 -0
  92. package/src/checks/automatic/textbox-name-present.js +236 -0
  93. package/src/checks/automatic/tooltip-name-present.js +158 -0
  94. package/src/checks/automatic/treeitem-name-present.js +157 -0
  95. package/src/checks/automatic/valid-lang.js +100 -0
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +193 -0
  97. package/src/checks/manual/accesskeys-manual.js +93 -0
  98. package/src/checks/manual/area-alt-decorative-manual.js +247 -0
  99. package/src/checks/manual/area-alt-quality-manual.js +204 -0
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +141 -0
  101. package/src/checks/manual/aria-text-manual.js +109 -0
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +170 -0
  103. package/src/checks/manual/css-hidden-focus.js +259 -0
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +204 -0
  105. package/src/checks/manual/empty-heading-manual.js +182 -0
  106. package/src/checks/manual/empty-table-header-manual.js +163 -0
  107. package/src/checks/manual/focus-order-semantics-manual.js +117 -0
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +291 -0
  109. package/src/checks/manual/heading-order-manual.js +130 -0
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +142 -0
  111. package/src/checks/manual/image-redundant-alt-manual.js +118 -0
  112. package/src/checks/manual/img-alt-decorative-manual.js +148 -0
  113. package/src/checks/manual/img-alt-quality-manual.js +182 -0
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +144 -0
  115. package/src/checks/manual/input-image-alt-quality-manual.js +144 -0
  116. package/src/checks/manual/label-title-only-manual.js +115 -0
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +180 -0
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +169 -0
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +167 -0
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +177 -0
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +169 -0
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +132 -0
  123. package/src/checks/manual/landmark-one-main-manual.js +151 -0
  124. package/src/checks/manual/landmark-unique-manual.js +252 -0
  125. package/src/checks/manual/link-name-quality-manual.js +143 -0
  126. package/src/checks/manual/media-transcript-present-manual.js +373 -0
  127. package/src/checks/manual/meta-viewport-large-manual.js +119 -0
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +134 -0
  129. package/src/checks/manual/no-autoplay-audio-manual.js +116 -0
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +194 -0
  131. package/src/checks/manual/p-as-heading-manual.js +163 -0
  132. package/src/checks/manual/page-has-heading-one-manual.js +110 -0
  133. package/src/checks/manual/page-title-patterns-manual.js +262 -0
  134. package/src/checks/manual/presentation-role-conflict-manual.js +159 -0
  135. package/src/checks/manual/region-manual.js +183 -0
  136. package/src/checks/manual/scope-attr-valid-manual.js +93 -0
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +168 -0
  138. package/src/checks/manual/skip-link-manual.js +150 -0
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +209 -0
  140. package/src/checks/manual/tabindex-manual.js +94 -0
  141. package/src/checks/manual/table-duplicate-name-manual.js +99 -0
  142. package/src/checks/manual/table-fake-caption-manual.js +122 -0
  143. package/src/checks/manual/video-caption-manual.js +118 -0
  144. package/src/checks/manual-review.js +95 -0
  145. package/src/checks/rules-and-tags.full.csv +19 -0
  146. package/src/checks/rules-and-tags.full.json +259 -0
  147. package/src/core/aria-helpers.js +906 -0
  148. package/src/core/contrast-helpers.js +1147 -0
  149. package/src/core/dom-helpers.js +4085 -0
  150. package/src/core/dom-runner.js +627 -0
  151. package/src/core/frame-messaging.js +210 -0
  152. package/src/core/frame-scan.js +178 -0
  153. package/src/core/rollup-composites.js +135 -0
  154. package/src/core/rule-meta.js +140 -0
  155. package/src/core.js +79055 -0
  156. package/src/coverage/wcag-facets.js +1079 -0
  157. package/src/coverage/wcag-version-map.js +84 -0
  158. package/src/i18n/en.js +919 -0
  159. package/src/i18n/fr.js +527 -0
  160. package/src/index.js +4 -0
  161. package/src/policy/contracts.js +18 -0
  162. package/src/policy/resolvePolicy.js +55 -0
  163. package/src/policy/schemas/engine-options.schema.json +103 -0
  164. package/src/policy/schemas/policy-contract.schema.json +40 -0
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check aria-conditional-attr
5
+ * @atomic true
6
+ * @summary aria-errormessage is only exposed when aria-invalid is not false/absent
7
+ * @standard WCAG 2.2
8
+ * @sc 4.1.2
9
+ * @applicability
10
+ * Elements with a non-empty `aria-errormessage` attribute.
11
+ * @expectation
12
+ * Per the ARIA specification, `aria-errormessage` is only exposed to
13
+ * assistive technology when `aria-invalid` is present with a value
14
+ * other than `"false"` (i.e. `"true"`, `"grammar"`, or `"spelling"`).
15
+ * An element with `aria-errormessage` but `aria-invalid` absent or
16
+ * `"false"` silently drops the error message from the accessibility
17
+ * tree — authors almost always intend it to be exposed.
18
+ * @implementation-notes
19
+ * - This is deliberately narrow: a widely-used reference engine's own
20
+ * `aria-conditional-attr` rule covers a broader table of attribute/condition
21
+ * pairs. This rule implements only the one pairing (`aria-errormessage` /
22
+ * `aria-invalid`) that is unambiguous and explicitly stated in the
23
+ * ARIA spec, to keep `fail` high-confidence — matches this repo's
24
+ * established pattern (see `aria-required-attr`/`aria-prohibited-attr`
25
+ * for the same "narrower than that reference engine, but zero false positives"
26
+ * trade-off).
27
+ * - Does not check whether the `aria-errormessage` ID reference itself
28
+ * resolves to an existing element — that is `aria-valid-attr-value`'s
29
+ * concern, not this rule's.
30
+ */
31
+
32
+ const id = 'aria-conditional-attr';
33
+
34
+ const meta = {
35
+ title: 'aria-errormessage requires aria-invalid to be set to a non-false value',
36
+ description:
37
+ 'Checks that elements with aria-errormessage also have aria-invalid set to "true", "grammar", or "spelling" — otherwise the error message is dropped from the accessibility tree.',
38
+ i18n: {
39
+ titleKey: 'ariaConditionalAttr_title',
40
+ descriptionKey: 'ariaConditionalAttr_description'
41
+ },
42
+ helpUrl: null,
43
+ tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
44
+ wcagSc: ['4.1.2'],
45
+ normativeMappings: [
46
+ { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
47
+ ],
48
+ defaultSeverity: 'serious',
49
+ category: 'robust',
50
+ type: 'automatic',
51
+ defaultConfidence: 'high',
52
+ coverage: { facetsBySc: { '4.1.2': ['aria-conditional-attr'] } }
53
+ };
54
+
55
+ function runInPage(ctx) {
56
+ const { document, root, helpers, rule } = ctx;
57
+ const safeRoot = root || document;
58
+
59
+ function trim(v) { return (v == null ? '' : String(v)).trim(); }
60
+
61
+ const TRUTHY_INVALID_VALUES = new Set(['true', 'grammar', 'spelling']);
62
+
63
+ const nodes = helpers.queryAllSmart
64
+ ? helpers.queryAllSmart('[aria-errormessage]', safeRoot)
65
+ : helpers.queryAll('[aria-errormessage]', safeRoot);
66
+
67
+ const occurrences = [];
68
+ let applicableCount = 0;
69
+
70
+ for (const el of nodes) {
71
+ if (!el || !el.getAttribute) continue;
72
+
73
+ const errorMessageRef = trim(el.getAttribute('aria-errormessage'));
74
+ if (!errorMessageRef) continue;
75
+
76
+ applicableCount += 1;
77
+
78
+ const invalidValue = trim(el.getAttribute('aria-invalid')).toLowerCase();
79
+ if (TRUTHY_INVALID_VALUES.has(invalidValue)) continue;
80
+
81
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
82
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
83
+ const tag = (el.tagName || '').toLowerCase();
84
+
85
+ occurrences.push({
86
+ selector: stableSelector,
87
+ html,
88
+ summary: 'This element has aria-errormessage but aria-invalid is missing or "false", so the error message is not exposed.',
89
+ hint: 'Set aria-invalid to "true" (or "grammar"/"spelling") whenever aria-errormessage should be exposed to assistive technology.',
90
+ i18n: {
91
+ summaryKey: 'ariaConditionalAttr_summary_fail',
92
+ hintKey: 'ariaConditionalAttr_hint_fail',
93
+ params: { element: tag, ariaInvalid: invalidValue || '(absent)' }
94
+ },
95
+ data: {
96
+ details: { reasonCode: 'ARIA_ERRORMESSAGE_WITHOUT_TRUTHY_INVALID', ariaInvalid: invalidValue }
97
+ }
98
+ });
99
+ }
100
+
101
+ if (applicableCount === 0) {
102
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
103
+ }
104
+ if (occurrences.length) {
105
+ return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
106
+ }
107
+ return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
108
+ }
109
+
110
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check aria-deprecated-role
5
+ * @atomic true
6
+ * @summary An explicit role attribute must not use a deprecated or author-prohibited ARIA role
7
+ * @standard WCAG 2.2
8
+ * @sc 4.1.2
9
+ * @applicability
10
+ * Applies to any element whose role attribute's first (used) token is a
11
+ * valid, non-abstract ARIA role that authors must never explicitly
12
+ * declare — either because WAI-ARIA has deprecated it (e.g. "directory",
13
+ * superseded by role="list") or because it's reserved for user-agent-
14
+ * internal use only, not a spec deprecation but the same "valid token,
15
+ * prohibited for authors" shape (role="generic" — verified 2026-07-20
16
+ * against both the WAI-ARIA 1.2 spec text and MDN's explicit "It should
17
+ * not be used by web authors" guidance).
18
+ * @expectation
19
+ * The role in use is neither deprecated nor author-prohibited. This is a
20
+ * distinct, atomic decision from aria-roles-valid (existence/
21
+ * abstractness): a role can be perfectly valid and non-abstract while
22
+ * still being off-limits for explicit author use.
23
+ */
24
+
25
+ const id = 'aria-deprecated-role';
26
+
27
+ const meta = {
28
+ title: 'role attribute must not use a deprecated or author-prohibited ARIA role',
29
+ description: 'Checks that an explicit role="" attribute does not use a role deprecated by the WAI-ARIA specification, or one reserved for user-agent-internal use only (e.g. role="generic").',
30
+ i18n: {
31
+ titleKey: 'ariaDeprecatedRole_title',
32
+ descriptionKey: 'ariaDeprecatedRole_description'
33
+ },
34
+ helpUrl: null,
35
+ tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
36
+ wcagSc: ['4.1.2'],
37
+ normativeMappings: [
38
+ { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
39
+ ],
40
+ defaultSeverity: 'moderate',
41
+ category: 'robust',
42
+ type: 'automatic',
43
+ defaultConfidence: 'high',
44
+ coverage: { facetsBySc: { '4.1.2': ['aria-role-not-deprecated'] } }
45
+ };
46
+
47
+ function runInPage(ctx) {
48
+ const { document, root, helpers, rule } = ctx;
49
+ const safeRoot = root || document;
50
+
51
+ const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
52
+ if (!ariaHelpers) {
53
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
54
+ }
55
+
56
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
57
+
58
+ const occurrences = [];
59
+ let applicableCount = 0;
60
+
61
+ for (const el of nodes) {
62
+ if (!el || !el.getAttribute) continue;
63
+
64
+ const role = ariaHelpers.getExplicitRole(el);
65
+ if (!role) continue;
66
+
67
+ // Only applicable to roles that are otherwise valid + concrete;
68
+ // invalid/abstract usage is aria-roles-valid's concern.
69
+ if (!ariaHelpers.isValidConcreteRole(role)) continue;
70
+
71
+ applicableCount += 1;
72
+
73
+ if (!ariaHelpers.isDeprecatedRole(role)) continue;
74
+
75
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
76
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
77
+ const guidance = ariaHelpers.getDeprecatedRoleGuidance
78
+ ? ariaHelpers.getDeprecatedRoleGuidance(role)
79
+ : 'Replace the deprecated role with its recommended replacement.';
80
+
81
+ occurrences.push({
82
+ selector: stableSelector,
83
+ html,
84
+ summary: `This element uses role="${role}", which authors must not explicitly declare.`,
85
+ hint: guidance,
86
+ i18n: {
87
+ summaryKey: 'ariaDeprecatedRole_summary_fail',
88
+ hintKey: 'ariaDeprecatedRole_hint_fail',
89
+ params: { role, guidance }
90
+ },
91
+ data: {
92
+ details: { reasonCode: 'ARIA_ROLE_DEPRECATED', role, guidance }
93
+ }
94
+ });
95
+ }
96
+
97
+ if (applicableCount === 0) {
98
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
99
+ }
100
+ if (occurrences.length) {
101
+ return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
102
+ }
103
+ return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
104
+ }
105
+
106
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check aria-hidden-body
5
+ * @atomic true
6
+ * @summary The document <body> must not have aria-hidden="true"
7
+ * @standard WCAG 2.2
8
+ * @sc 1.3.1, 4.1.2
9
+ * @applicability
10
+ * Always applicable to any HTML document with a <body> element,
11
+ * independent of contextSelector/root scoping — this is a whole-
12
+ * document concern, matching page-title-present's pattern of
13
+ * evaluating document.body directly rather than the scoped root.
14
+ * @expectation
15
+ * <body> does not have aria-hidden="true". Hiding the document body
16
+ * removes the entire page's content and structure from the
17
+ * accessibility tree at once — a widely-used reference engine tags this under both 1.3.1
18
+ * (Info and Relationships: the page's structure becomes entirely
19
+ * non-determinable) and 4.1.2 (Name, Role, Value: nothing in the
20
+ * document exposes a role/name/value any longer).
21
+ */
22
+
23
+ const id = 'aria-hidden-body';
24
+
25
+ const meta = {
26
+ title: 'The document <body> must not be aria-hidden',
27
+ description: 'Checks that <body> does not have aria-hidden="true", which would remove the entire page from the accessibility tree.',
28
+ i18n: {
29
+ titleKey: 'ariaHiddenBody_title',
30
+ descriptionKey: 'ariaHiddenBody_description'
31
+ },
32
+ helpUrl: null,
33
+ tags: ['wcag2a', 'wcag131', 'wcag412', 'structure', 'atomic', 'automatic'],
34
+ wcagSc: ['1.3.1', '4.1.2'],
35
+ normativeMappings: [
36
+ { standard: 'WCAG', version: '2.2', requirement: '1.3.1', title: 'Info and Relationships', conformanceLevel: 'A' },
37
+ { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
38
+ ],
39
+ defaultSeverity: 'critical',
40
+ category: 'perceivable',
41
+ type: 'automatic',
42
+ defaultConfidence: 'high',
43
+ coverage: {
44
+ facetsBySc: {
45
+ '1.3.1': ['aria-hidden-body-absent'],
46
+ '4.1.2': ['aria-hidden-body-absent']
47
+ }
48
+ }
49
+ };
50
+
51
+ function runInPage(ctx) {
52
+ const { document, helpers, rule } = ctx;
53
+
54
+ const body = document && document.body ? document.body : null;
55
+ if (!body) {
56
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57
+ }
58
+
59
+ const raw = body.getAttribute ? body.getAttribute('aria-hidden') : null;
60
+ const isHidden = raw != null && String(raw).trim().toLowerCase() === 'true';
61
+
62
+ if (!isHidden) {
63
+ return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
64
+ }
65
+
66
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
67
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(body) : (body.outerHTML || '').slice(0, 200);
68
+
69
+ const occurrences = [{
70
+ selector: stableSelector,
71
+ html,
72
+ summary: 'The document body has aria-hidden="true", which hides the entire page from assistive technologies.',
73
+ hint: 'Remove aria-hidden from <body>. Hide specific elements instead, if that was the intent.',
74
+ i18n: {
75
+ summaryKey: 'ariaHiddenBody_summary_fail',
76
+ hintKey: 'ariaHiddenBody_hint_fail',
77
+ params: {}
78
+ },
79
+ data: {
80
+ details: { reasonCode: 'ARIA_HIDDEN_BODY' }
81
+ }
82
+ }];
83
+
84
+ return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'critical', occurrences };
85
+ }
86
+
87
+ module.exports = { id, meta, runInPage };