@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,148 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check img-alt-decorative
5
+ * @atomic true
6
+ * @summary Manual review: text alternative appropriateness (WCAG 1.1.1)
7
+ * @standard WCAG 2.2
8
+ * @sc 1.1.1
9
+ * @type manual
10
+ * @expectation
11
+ * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
+ */
13
+
14
+ const id = "img-alt-decorative";
15
+
16
+ const meta = {
17
+ title: "<img> with alt=\"\" must be decorative (manual review)",
18
+ description: "Flags <img> elements with empty alt for human review that they are purely decorative.",
19
+ i18n: {
20
+ titleKey: "img_altDecorative_title",
21
+ descriptionKey: "img_altDecorative_description"
22
+ },
23
+ helpUrl: null,
24
+ tags: ["wcag2a", "wcag111", "nontext", "images", "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
+ }
37
+ }
38
+ };
39
+
40
+ 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 { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
50
+ catch { return []; }
51
+ };
52
+
53
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
54
+ ? helpers.isAccTreeEligible
55
+ : null;
56
+
57
+ const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
58
+ ? helpers.getFocusableInfo
59
+ : null;
60
+
61
+ function isRolePresentationExcluded(el) {
62
+ const role = (() => {
63
+ try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
64
+ catch { return ''; }
65
+ })();
66
+ if (role !== 'presentation' && role !== 'none') return false;
67
+
68
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
69
+ let focusable = false;
70
+ if (getFocusableInfo) {
71
+ const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
72
+ focusable = !!(fi && fi.focusable);
73
+ } else {
74
+ const tabindex = el.getAttribute('tabindex');
75
+ focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
76
+ }
77
+ return !focusable;
78
+ }
79
+
80
+ const els = (() => {
81
+ // Only likely candidates:
82
+ // - alt="" (exact)
83
+ // - alt that starts/ends with space (to catch whitespace-only like " ")
84
+ const sel = 'img[alt=""], img[alt^=" "], img[alt$=" "]';
85
+ try { return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []); }
86
+ catch { return queryAll(sel); }
87
+ })();
88
+ const uniqueEls = [];
89
+ const seen = new Set();
90
+ for (const el of els) {
91
+ if (!seen.has(el)) { seen.add(el); uniqueEls.push(el); }
92
+ }
93
+
94
+ if (!els.length) {
95
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
96
+ }
97
+
98
+ const occurrences = [];
99
+ let applicableCount = 0;
100
+
101
+ for (const el of uniqueEls) {
102
+ if (!el || !el.getAttribute) continue;
103
+
104
+ if (isAccTreeEligible) {
105
+ const elig = (() => {
106
+ try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
107
+ })();
108
+ if (elig && elig.eligible === false) continue;
109
+ }
110
+
111
+ if (isRolePresentationExcluded(el)) continue;
112
+
113
+ // Rule-specific applicability (only elements that already have a text alternative mechanism)
114
+ const rawAlt = el.getAttribute('alt');
115
+ if (rawAlt == null) continue;
116
+ if (String(rawAlt).trim() !== '') continue;
117
+
118
+ applicableCount += 1;
119
+
120
+ const baseOccurrence = {
121
+ summary: 'Review whether <img> is decorative (alt="").',
122
+ hint: 'Confirm the image is purely decorative. If it conveys information or function, provide meaningful alt text.',
123
+ i18n: {
124
+ summaryKey: 'img_altDecorative_summary_cantTell',
125
+ hintKey: 'img_altDecorative_hint_cantTell',
126
+ params: { element: 'img' }
127
+ },
128
+ data: {
129
+ visibilityFilter: { targetSet: 'acc', accEligible: null, reasons: [] },
130
+ details: null
131
+ }
132
+ };
133
+
134
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
135
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
136
+ } else {
137
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
138
+ }
139
+ }
140
+
141
+ if (applicableCount === 0) {
142
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
143
+ }
144
+
145
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
146
+ }
147
+
148
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check img-alt-quality
5
+ * @atomic true
6
+ * @summary Manual review: text alternative appropriateness (WCAG 1.1.1)
7
+ * @standard WCAG 2.2
8
+ * @sc 1.1.1
9
+ * @type manual
10
+ * @expectation
11
+ * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
+ */
13
+
14
+ const id = "img-alt-quality";
15
+
16
+ const meta = {
17
+ title: "<img> alt text must be appropriate (manual review)",
18
+ description: "Flags <img> elements with non-empty alt text for human review of appropriateness.",
19
+ i18n: {
20
+ titleKey: "img_altQuality_title",
21
+ descriptionKey: "img_altQuality_description"
22
+ },
23
+ helpUrl: null,
24
+ tags: ["wcag2a", "wcag111", "nontext", "images", "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
+ }
37
+ }
38
+ };
39
+
40
+ function runInPage(ctx) {
41
+ const { document, root, helpers, rule } = ctx;
42
+ const safeRoot = root || document;
43
+
44
+ // Cap occurrences to keep manual “quality” checks fast on large pages.
45
+ // Deterministic: we keep DOM order, just stop collecting after N.
46
+ const MAX_OCCURRENCES = 50;
47
+
48
+ const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
49
+ const queryAll = helpers && typeof helpers.queryAll === 'function'
50
+ ? helpers.queryAll
51
+ : (sel) => {
52
+ try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
53
+ catch { return []; }
54
+ };
55
+
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 { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
71
+ catch { return ''; }
72
+ })();
73
+ if (role !== 'presentation' && role !== 'none') return false;
74
+
75
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
76
+ let focusable = false;
77
+ if (getFocusableInfo) {
78
+ const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
79
+ focusable = !!(fi && fi.focusable);
80
+ } else {
81
+ const tabindex = el.getAttribute('tabindex');
82
+ focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
83
+ }
84
+ return !focusable;
85
+ }
86
+
87
+ const selector = 'img[alt]:not([alt=""])';
88
+ const els = (() => {
89
+ try { return Array.from((queryAllSmart ? queryAllSmart(selector) : queryAll(selector)) || []); }
90
+ catch { return queryAll(selector); }
91
+ })();
92
+
93
+ if (!els.length) {
94
+ return {
95
+ ruleId: rule.ruleId,
96
+ outcome: 'notApplicable',
97
+ severity: 'minor',
98
+ occurrences: [],
99
+ data: { details: { applicableCount: 0, reportedCount: 0, maxOccurrences: MAX_OCCURRENCES, truncated: false } }
100
+ };
101
+ }
102
+
103
+ const occurrences = [];
104
+ let applicableCount = 0; // total applicable elements
105
+ let collectedCount = 0; // how many occurrences we actually reported
106
+
107
+ for (const el of els) {
108
+ if (!el || !el.getAttribute) continue;
109
+
110
+ if (isAccTreeEligible) {
111
+ const elig = (() => {
112
+ try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
113
+ })();
114
+ if (elig && elig.eligible === false) continue;
115
+ }
116
+
117
+ if (isRolePresentationExcluded(el)) continue;
118
+
119
+ // Rule-specific applicability: non-empty alt
120
+ const alt = (() => {
121
+ try { return String(el.getAttribute('alt') || '').trim(); } catch { return ''; }
122
+ })();
123
+ if (!alt) continue;
124
+
125
+ applicableCount += 1;
126
+
127
+ // IMPORTANT: stop doing expensive occurrence building after we hit the cap
128
+ if (collectedCount >= MAX_OCCURRENCES) continue;
129
+
130
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
131
+ const baseOccurrence = {
132
+ summary: 'Review alt text on <img> for accuracy and appropriateness.',
133
+ hint: 'Ensure the alt text conveys the image’s purpose/information in context (not redundant, not filename-like).',
134
+ i18n: {
135
+ summaryKey: 'img_altQuality_summary_cantTell',
136
+ hintKey: 'img_altQuality_hint_cantTell',
137
+ params: { element: 'img' }
138
+ },
139
+ data: {
140
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
141
+ details: null
142
+ }
143
+ };
144
+
145
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
146
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
147
+ } else {
148
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
149
+ }
150
+
151
+ collectedCount += 1;
152
+ }
153
+
154
+ if (applicableCount === 0) {
155
+ return {
156
+ ruleId: rule.ruleId,
157
+ outcome: 'notApplicable',
158
+ severity: 'minor',
159
+ occurrences: [],
160
+ data: { details: { applicableCount: 0, reportedCount: 0, maxOccurrences: MAX_OCCURRENCES, truncated: false } }
161
+ };
162
+ }
163
+
164
+ const truncated = applicableCount > collectedCount;
165
+
166
+ return {
167
+ ruleId: rule.ruleId,
168
+ outcome: 'cantTell',
169
+ severity: 'minor',
170
+ occurrences,
171
+ data: {
172
+ details: {
173
+ applicableCount,
174
+ reportedCount: collectedCount,
175
+ maxOccurrences: MAX_OCCURRENCES,
176
+ truncated
177
+ }
178
+ }
179
+ };
180
+ }
181
+
182
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check input-image-alt-decorative
5
+ * @atomic true
6
+ * @summary Manual review: text alternative appropriateness (WCAG 1.1.1)
7
+ * @standard WCAG 2.2
8
+ * @sc 1.1.1
9
+ * @type manual
10
+ * @expectation
11
+ * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
+ */
13
+
14
+ const id = "input-image-alt-decorative";
15
+
16
+ const meta = {
17
+ title: "<input type=\"image\"> with alt=\"\" must be appropriate (manual review)",
18
+ description: "Flags <input type=\"image\"> elements with empty alt for human review (usually not appropriate for functional controls).",
19
+ i18n: {
20
+ titleKey: "inputImage_altDecorative_title",
21
+ descriptionKey: "inputImage_altDecorative_description"
22
+ },
23
+ helpUrl: null,
24
+ tags: ["wcag2a", "wcag111", "forms", "images", "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
+ }
37
+ }
38
+ };
39
+
40
+ 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 { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
50
+ catch { return []; }
51
+ };
52
+
53
+ const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
54
+ ? helpers.getEligibilityInfo
55
+ : null;
56
+
57
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
58
+ ? helpers.isAccTreeEligible
59
+ : null;
60
+
61
+ const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
62
+ ? helpers.getFocusableInfo
63
+ : null;
64
+
65
+ function isRolePresentationExcluded(el) {
66
+ const role = (() => {
67
+ try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
68
+ catch { return ''; }
69
+ })();
70
+ if (role !== 'presentation' && role !== 'none') return false;
71
+
72
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
73
+ let focusable = false;
74
+ if (getFocusableInfo) {
75
+ const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
76
+ focusable = !!(fi && fi.focusable);
77
+ } else {
78
+ const tabindex = el.getAttribute('tabindex');
79
+ focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
80
+ }
81
+ return !focusable;
82
+ }
83
+
84
+
85
+ const els = (() => {
86
+ try { return Array.from((queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) || []); }
87
+ catch { return queryAll('input[type="image"]'); }
88
+ })();
89
+
90
+ if (!els.length) {
91
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
92
+ }
93
+
94
+ const occurrences = [];
95
+ let applicableCount = 0;
96
+
97
+ for (const el of els) {
98
+ if (!el || !el.getAttribute) continue;
99
+
100
+ if (isAccTreeEligible) {
101
+ const elig = (() => {
102
+ try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
103
+ })();
104
+ if (elig && elig.eligible === false) continue;
105
+ }
106
+
107
+ if (isRolePresentationExcluded(el)) continue;
108
+
109
+ // Rule-specific applicability (only elements that already have a text alternative mechanism)
110
+ if (!((el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() === ''))) continue;
111
+
112
+ applicableCount += 1;
113
+
114
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
115
+
116
+ const baseOccurrence = {
117
+ summary: 'Review <input type="image"> with alt="".',
118
+ hint: 'This control is typically functional. Confirm it has an equivalent accessible name elsewhere, or provide meaningful alt text.',
119
+ i18n: {
120
+ summaryKey: 'inputImage_altDecorative_summary_cantTell',
121
+ hintKey: 'inputImage_altDecorative_hint_cantTell',
122
+ params: { element: 'input[type=image]' }
123
+ },
124
+ data: {
125
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
126
+ details: null
127
+ }
128
+ };
129
+
130
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
131
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
132
+ } else {
133
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
134
+ }
135
+ }
136
+
137
+ if (applicableCount === 0) {
138
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
139
+ }
140
+
141
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
142
+ }
143
+
144
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check input-image-alt-quality
5
+ * @atomic true
6
+ * @summary Manual review: text alternative appropriateness (WCAG 1.1.1)
7
+ * @standard WCAG 2.2
8
+ * @sc 1.1.1
9
+ * @type manual
10
+ * @expectation
11
+ * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
+ */
13
+
14
+ const id = "input-image-alt-quality";
15
+
16
+ const meta = {
17
+ title: "<input type=\"image\"> alt text must be appropriate (manual review)",
18
+ description: "Flags <input type=\"image\"> elements with non-empty alt text for human review of appropriateness.",
19
+ i18n: {
20
+ titleKey: "inputImage_altQuality_title",
21
+ descriptionKey: "inputImage_altQuality_description"
22
+ },
23
+ helpUrl: null,
24
+ tags: ["wcag2a", "wcag111", "forms", "images", "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
+ }
37
+ }
38
+ };
39
+
40
+ 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 { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
50
+ catch { return []; }
51
+ };
52
+
53
+ const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
54
+ ? helpers.getEligibilityInfo
55
+ : null;
56
+
57
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
58
+ ? helpers.isAccTreeEligible
59
+ : null;
60
+
61
+ const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
62
+ ? helpers.getFocusableInfo
63
+ : null;
64
+
65
+ function isRolePresentationExcluded(el) {
66
+ const role = (() => {
67
+ try { return String(el.getAttribute('role') || '').trim().toLowerCase(); }
68
+ catch { return ''; }
69
+ })();
70
+ if (role !== 'presentation' && role !== 'none') return false;
71
+
72
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
73
+ let focusable = false;
74
+ if (getFocusableInfo) {
75
+ const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
76
+ focusable = !!(fi && fi.focusable);
77
+ } else {
78
+ const tabindex = el.getAttribute('tabindex');
79
+ focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
80
+ }
81
+ return !focusable;
82
+ }
83
+
84
+
85
+ const els = (() => {
86
+ try { return Array.from((queryAllSmart ? queryAllSmart('input[type="image"]') : queryAll('input[type="image"]')) || []); }
87
+ catch { return queryAll('input[type="image"]'); }
88
+ })();
89
+
90
+ if (!els.length) {
91
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
92
+ }
93
+
94
+ const occurrences = [];
95
+ let applicableCount = 0;
96
+
97
+ for (const el of els) {
98
+ if (!el || !el.getAttribute) continue;
99
+
100
+ if (isAccTreeEligible) {
101
+ const elig = (() => {
102
+ try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
103
+ })();
104
+ if (elig && elig.eligible === false) continue;
105
+ }
106
+
107
+ if (isRolePresentationExcluded(el)) continue;
108
+
109
+ // Rule-specific applicability (only elements that already have a text alternative mechanism)
110
+ if (!((el.getAttribute('alt') != null && String(el.getAttribute('alt')).trim() !== ''))) continue;
111
+
112
+ applicableCount += 1;
113
+
114
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
115
+
116
+ const baseOccurrence = {
117
+ summary: 'Review alt text on <input type="image"> for accuracy and appropriateness.',
118
+ hint: 'Ensure the alt text describes the control’s action (e.g., “Search”, “Submit order”) in context.',
119
+ i18n: {
120
+ summaryKey: 'inputImage_altQuality_summary_cantTell',
121
+ hintKey: 'inputImage_altQuality_hint_cantTell',
122
+ params: { element: 'input[type=image]' }
123
+ },
124
+ data: {
125
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
126
+ details: { alt: String(el.getAttribute('alt') || '') } // optional but useful
127
+ }
128
+ };
129
+
130
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
131
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
132
+ } else {
133
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
134
+ }
135
+ }
136
+
137
+ if (applicableCount === 0) {
138
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
139
+ }
140
+
141
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
142
+ }
143
+
144
+ module.exports = { id, meta, runInPage };