@surea11y/core 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +240 -149
  2. package/README.md +51 -44
  3. package/docs/ACT_RULE_MAPPING.md +245 -0
  4. package/docs/API_STABILITY.md +53 -5
  5. package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
  6. package/docs/DESIGN_CHALLENGES.md +367 -0
  7. package/docs/EARL.md +100 -0
  8. package/docs/ENGINE_OPTIONS.md +42 -4
  9. package/docs/I18N.md +4 -4
  10. package/docs/INTEGRATION.md +4 -2
  11. package/docs/LIMITATIONS.md +9 -5
  12. package/docs/OUTPUT_SCHEMA.md +44 -6
  13. package/docs/POLICY.md +1 -1
  14. package/docs/REPORT.md +1 -1
  15. package/docs/RULE_AUTHORING.md +63 -36
  16. package/docs/RULE_CATALOG.md +1928 -169
  17. package/docs/RULE_HELPERS.md +333 -0
  18. package/docs/RULE_TAXONOMY.md +27 -6
  19. package/docs/SARIF.md +21 -2
  20. package/docs/TROUBLESHOOTING.md +2 -2
  21. package/docs/WCAG_CONFORMANCE.md +34 -10
  22. package/package.json +11 -9
  23. package/src/baseline.js +3 -3
  24. package/src/checks/automatic/area-alt-present.js +2 -2
  25. package/src/checks/automatic/aria-allowed-attr.js +74 -10
  26. package/src/checks/automatic/aria-allowed-role.js +34 -25
  27. package/src/checks/automatic/aria-braille-equivalent.js +21 -13
  28. package/src/checks/automatic/aria-conditional-attr.js +22 -15
  29. package/src/checks/automatic/aria-deprecated-role.js +13 -1
  30. package/src/checks/automatic/aria-hidden-body.js +3 -3
  31. package/src/checks/automatic/aria-hidden-focus.js +5 -5
  32. package/src/checks/automatic/aria-prohibited-attr.js +23 -18
  33. package/src/checks/automatic/aria-prohibited-children.js +136 -43
  34. package/src/checks/automatic/aria-required-attr.js +119 -24
  35. package/src/checks/automatic/aria-required-children.js +54 -30
  36. package/src/checks/automatic/aria-required-parent.js +93 -15
  37. package/src/checks/automatic/aria-role-name-present.js +37 -23
  38. package/src/checks/automatic/aria-roles-valid.js +52 -21
  39. package/src/checks/automatic/aria-valid-attr-value.js +89 -33
  40. package/src/checks/automatic/aria-valid-attr.js +15 -10
  41. package/src/checks/automatic/autocomplete-valid.js +2 -2
  42. package/src/checks/automatic/avoid-inline-spacing.js +133 -6
  43. package/src/checks/automatic/binary-control-name-present.js +27 -5
  44. package/src/checks/automatic/button-name-present.js +92 -6
  45. package/src/checks/automatic/combobox-name-present.js +26 -6
  46. package/src/checks/automatic/contrast-computable.js +42 -0
  47. package/src/checks/automatic/contrast-enhanced.js +33 -1
  48. package/src/checks/automatic/contrast-minimum.js +33 -1
  49. package/src/checks/automatic/css-orientation-lock.js +138 -24
  50. package/src/checks/automatic/definition-list-children-valid.js +7 -8
  51. package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
  52. package/src/checks/automatic/dialog-name-present.js +20 -2
  53. package/src/checks/automatic/duplicate-id-aria.js +10 -3
  54. package/src/checks/automatic/duplicate-id.js +203 -0
  55. package/src/checks/automatic/embed-text-alternative-present.js +2 -2
  56. package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
  57. package/src/checks/automatic/form-control-single-label.js +10 -1
  58. package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
  59. package/src/checks/automatic/iframe-focusable-content.js +68 -7
  60. package/src/checks/automatic/iframe-name-present.js +37 -3
  61. package/src/checks/automatic/iframe-title-unique.js +1 -1
  62. package/src/checks/automatic/img-alt-present.js +12 -4
  63. package/src/checks/automatic/label-in-name.js +204 -68
  64. package/src/checks/automatic/link-in-text-block.js +285 -29
  65. package/src/checks/automatic/link-name-present.js +22 -1
  66. package/src/checks/automatic/list-children-valid.js +6 -6
  67. package/src/checks/automatic/listbox-name-present.js +28 -8
  68. package/src/checks/automatic/listitem-parent-valid.js +4 -4
  69. package/src/checks/automatic/menuitem-name-present.js +20 -2
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
  72. package/src/checks/automatic/meter-name-present.js +23 -4
  73. package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
  74. package/src/checks/automatic/option-name-present.js +23 -4
  75. package/src/checks/automatic/page-title-present.js +21 -3
  76. package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
  77. package/src/checks/automatic/progressbar-name-present.js +23 -4
  78. package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
  79. package/src/checks/automatic/searchbox-name-present.js +28 -8
  80. package/src/checks/automatic/server-side-image-map-absent.js +1 -1
  81. package/src/checks/automatic/slider-name-present.js +27 -6
  82. package/src/checks/automatic/spinbutton-name-present.js +28 -8
  83. package/src/checks/automatic/summary-name-present.js +18 -2
  84. package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
  85. package/src/checks/automatic/svg-text-alternative-present.js +13 -10
  86. package/src/checks/automatic/tab-name-present.js +21 -2
  87. package/src/checks/automatic/table-headers-attr-valid.js +43 -8
  88. package/src/checks/automatic/table-th-has-data-cells.js +61 -5
  89. package/src/checks/automatic/target-size-minimum.js +155 -58
  90. package/src/checks/automatic/td-has-header.js +24 -23
  91. package/src/checks/automatic/textbox-name-present.js +28 -8
  92. package/src/checks/automatic/tooltip-name-present.js +21 -2
  93. package/src/checks/automatic/treeitem-name-present.js +23 -4
  94. package/src/checks/automatic/valid-lang.js +92 -7
  95. package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
  96. package/src/checks/manual/accesskeys-manual.js +3 -3
  97. package/src/checks/manual/area-alt-decorative-manual.js +7 -0
  98. package/src/checks/manual/area-alt-quality-manual.js +6 -0
  99. package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
  100. package/src/checks/manual/aria-text-manual.js +4 -4
  101. package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
  103. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
  105. package/src/checks/manual/empty-heading-manual.js +58 -11
  106. package/src/checks/manual/empty-table-header-manual.js +8 -8
  107. package/src/checks/manual/focus-order-semantics-manual.js +15 -15
  108. package/src/checks/manual/form-control-label-quality-manual.js +563 -0
  109. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
  110. package/src/checks/manual/heading-order-manual.js +3 -3
  111. package/src/checks/manual/heading-quality-manual.js +338 -0
  112. package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
  113. package/src/checks/manual/image-redundant-alt-manual.js +4 -4
  114. package/src/checks/manual/img-alt-decorative-manual.js +211 -52
  115. package/src/checks/manual/img-alt-quality-manual.js +7 -0
  116. package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
  117. package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
  118. package/src/checks/manual/label-title-only-manual.js +4 -4
  119. package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
  120. package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
  121. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
  122. package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
  123. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
  124. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
  125. package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
  126. package/src/checks/manual/landmark-one-main-manual.js +6 -6
  127. package/src/checks/manual/landmark-unique-manual.js +9 -9
  128. package/src/checks/manual/link-name-quality-manual.js +161 -32
  129. package/src/checks/manual/media-transcript-present-manual.js +2 -3
  130. package/src/checks/manual/meta-viewport-large-manual.js +2 -2
  131. package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
  132. package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
  133. package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
  134. package/src/checks/manual/p-as-heading-manual.js +4 -4
  135. package/src/checks/manual/page-has-heading-one-manual.js +6 -6
  136. package/src/checks/manual/page-title-patterns-manual.js +26 -3
  137. package/src/checks/manual/password-paste-enabled-manual.js +255 -0
  138. package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
  139. package/src/checks/manual/region-manual.js +19 -19
  140. package/src/checks/manual/scope-attr-valid-manual.js +2 -2
  141. package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
  142. package/src/checks/manual/skip-link-manual.js +5 -5
  143. package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
  144. package/src/checks/manual/tabindex-manual.js +2 -2
  145. package/src/checks/manual/table-duplicate-name-manual.js +2 -2
  146. package/src/checks/manual/table-fake-caption-manual.js +2 -2
  147. package/src/checks/manual/video-caption-manual.js +3 -3
  148. package/src/checks/manual-review.js +17 -1
  149. package/src/core.js +8880 -41883
  150. package/src/earl.js +144 -0
  151. package/src/report.js +2 -2
  152. package/src/sarif.js +22 -2
  153. package/surea11y.browser.js +10 -37882
  154. package/surea11y.i18n.de.js +2 -21
  155. package/surea11y.i18n.es.js +2 -21
  156. package/surea11y.i18n.fr.js +2 -21
  157. package/bin/surea11y-core.js +0 -20
package/src/earl.js ADDED
@@ -0,0 +1,144 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
3
+ 'use strict';
4
+
5
+ /**
6
+ * Renders scan results as an EARL 1.0 report in JSON-LD (docs/EARL.md), the
7
+ * format the W3C ACT Rules community group accepts as an implementation
8
+ * report and the interchange format most accessibility tooling reads.
9
+ *
10
+ * The graph is grouped by TestSubject rather than being a flat list of
11
+ * assertions: one entry per scanned URL, each carrying the assertions made
12
+ * about it. That is what the ACT earl-context expects, and it is why this
13
+ * takes an array of results as readily as one -- a report covering many pages
14
+ * is the normal case, not an extension.
15
+ *
16
+ * Every rule that ran becomes an assertion, including `pass` and
17
+ * `notApplicable`. That is the opposite of the SARIF and HTML reporters,
18
+ * which carry violations only: an implementation report is a claim about what
19
+ * the engine decided everywhere, and a rule that was silent because it found
20
+ * nothing applicable is evidence, not noise.
21
+ */
22
+
23
+ const EARL_CONTEXT = 'https://www.w3.org/WAI/content-assets/wcag-act-rules/earl-context.json';
24
+
25
+ // The engine's four outcomes onto EARL's vocabulary. `earl:untested` has no
26
+ // counterpart here: a rule that did not run produces no result to assert on.
27
+ const OUTCOME_TO_EARL = {
28
+ pass: 'earl:passed',
29
+ fail: 'earl:failed',
30
+ cantTell: 'earl:cantTell',
31
+ notApplicable: 'earl:inapplicable'
32
+ };
33
+
34
+ /**
35
+ * A Success Criterion's own id, as WCAG publishes it: the slug of its title.
36
+ * Derived rather than tabulated so a rule added with a new SC needs no second
37
+ * edit here, and pinned by a test that lists every slug the catalog produces.
38
+ */
39
+ function scSlug(title) {
40
+ return String(title || '')
41
+ .toLowerCase()
42
+ .replace(/[(),.:]/g, '')
43
+ .replace(/[^a-z0-9]+/g, '-')
44
+ .replace(/^-+|-+$/g, '');
45
+ }
46
+
47
+ /**
48
+ * `normativeMappings` also carries Understanding-document references and
49
+ * non-WCAG standards, which share `standard: 'WCAG'` and a `requirement` with
50
+ * the real thing. A Success Criterion is the entry that states a conformance
51
+ * level and claims no other document type.
52
+ */
53
+ function wcagCriteria(check) {
54
+ const mappings = (check && check.meta && check.meta.normativeMappings) || [];
55
+ const slugs = new Set();
56
+
57
+ for (const m of mappings) {
58
+ if (!m || m.standard !== 'WCAG' || m.type || !m.conformanceLevel) continue;
59
+ const slug = scSlug(m.title);
60
+ if (slug) slugs.add(`WCAG2:${slug}`);
61
+ }
62
+
63
+ return [...slugs].sort();
64
+ }
65
+
66
+ function assertionFor(check, assertor, mode) {
67
+ const outcome = OUTCOME_TO_EARL[check.outcome];
68
+ if (!outcome) return null;
69
+
70
+ const test = { title: check.ruleId };
71
+ const isPartOf = wcagCriteria(check);
72
+ if (isPartOf.length) test.isPartOf = isPartOf;
73
+
74
+ const assertion = {
75
+ '@type': 'Assertion',
76
+ test,
77
+ result: { outcome }
78
+ };
79
+
80
+ if (assertor) assertion.assertedBy = assertor;
81
+ if (mode) assertion.mode = mode;
82
+
83
+ return assertion;
84
+ }
85
+
86
+ function normalizeAssertor(options) {
87
+ if (options.assertor === null) return null;
88
+
89
+ const supplied = options.assertor || {};
90
+ const assertor = { '@type': 'Assertor', name: supplied.name || 'surea11y' };
91
+ const revision = supplied.version || supplied.revision;
92
+ if (revision) assertor.release = { '@type': 'Version', revision: String(revision) };
93
+ return assertor;
94
+ }
95
+
96
+ /**
97
+ * @param {object|object[]} results one scan result, or several to report together
98
+ * @param {object} [options]
99
+ * @param {object|null} [options.assertor] `{ name, version }`; null omits it
100
+ * @param {string} [options.mode] an EARL test mode, e.g. `'earl:automatic'`
101
+ * @returns {object} the JSON-LD document
102
+ */
103
+ function renderEarlReport(results, options = {}) {
104
+ const list = (Array.isArray(results) ? results : [results]).filter(
105
+ (r) => r && typeof r === 'object'
106
+ );
107
+
108
+ const assertor = normalizeAssertor(options);
109
+ const mode = typeof options.mode === 'string' && options.mode ? options.mode : null;
110
+
111
+ // Several results for one URL merge into a single subject: a caller scanning
112
+ // the same page under different engineOptions still describes one resource,
113
+ // and the context has no way to express two subjects with the same source.
114
+ const bySource = new Map();
115
+
116
+ for (const result of list) {
117
+ const source = typeof result.url === 'string' && result.url ? result.url : 'about:blank';
118
+ const checks = Array.isArray(result.checksResults) ? result.checksResults : [];
119
+
120
+ if (!bySource.has(source)) bySource.set(source, new Map());
121
+ const assertions = bySource.get(source);
122
+
123
+ for (const check of checks) {
124
+ if (!check || typeof check.ruleId !== 'string') continue;
125
+ const assertion = assertionFor(check, assertor, mode);
126
+ if (assertion) assertions.set(check.ruleId, assertion);
127
+ }
128
+ }
129
+
130
+ const graph = [...bySource.keys()]
131
+ .sort()
132
+ .map((source) => ({
133
+ '@type': 'TestSubject',
134
+ source,
135
+ assertions: [...bySource.get(source).keys()]
136
+ .sort()
137
+ .map((ruleId) => bySource.get(source).get(ruleId))
138
+ }))
139
+ .filter((subject) => subject.assertions.length);
140
+
141
+ return { '@context': EARL_CONTEXT, '@graph': graph };
142
+ }
143
+
144
+ module.exports = { renderEarlReport, EARL_CONTEXT, OUTCOME_TO_EARL, scSlug };
package/src/report.js CHANGED
@@ -282,7 +282,7 @@ function renderCards(checksResults) {
282
282
  <div class="card-meta">${wcagChips}</div>
283
283
  <div class="card-selector"><span class="card-selector-label">Selector:</span> <code>${esc(representative.selector || '(none)')}</code></div>
284
284
  <div class="card-snippet">${esc(representative.summary)}${representative.hint ? ` — ${esc(representative.hint)}` : ''}</div>
285
- ${r.occurrences.length > 1 ? `<p class="card-note">Selector/summary above are from one representative occurrence — ${r.occurrences.length.toLocaleString()} total on this rule.</p>` : ''}
285
+ ${r.occurrences.length > 1 ? `<p class="card-note">Selector/summary above are from one representative occurrence; ${r.occurrences.length.toLocaleString()} total on this rule.</p>` : ''}
286
286
  </div>
287
287
  </div>`;
288
288
  })
@@ -290,7 +290,7 @@ function renderCards(checksResults) {
290
290
 
291
291
  const overflow =
292
292
  sorted.length > MAX_CARDS
293
- ? `<p class="note">Showing the ${MAX_CARDS} highest-priority rules of ${sorted.length.toLocaleString()} with issues — see the full technical data below for the rest.</p>`
293
+ ? `<p class="note">Showing the ${MAX_CARDS} highest-priority rules of ${sorted.length.toLocaleString()} with issues. See the full technical data below for the rest.</p>`
294
294
  : '';
295
295
 
296
296
  return `<div class="cards">${cards}</div>${overflow}`;
package/src/sarif.js CHANGED
@@ -133,6 +133,7 @@ function renderSarifReport(result, options = {}) {
133
133
  const seenRuleIds = new Set();
134
134
  const failResults = [];
135
135
  const cantTellResults = [];
136
+ const notices = [];
136
137
 
137
138
  for (const check of (result && result.checksResults) || []) {
138
139
  if (!check || !Array.isArray(check.occurrences)) continue;
@@ -142,7 +143,23 @@ function renderSarifReport(result, options = {}) {
142
143
  rules.push(buildRule(check));
143
144
  }
144
145
 
145
- if (check.outcome !== 'fail' && check.outcome !== 'cantTell') continue;
146
+ if (check.outcome !== 'fail' && check.outcome !== 'cantTell') {
147
+ // A rule with nothing to judge may still say why, which is the
148
+ // difference between "checked, nothing to flag" and "could not check".
149
+ // That is not an alert, so it cannot be a result; carrying it as an
150
+ // execution notice keeps a SARIF-only pipeline from reading silence as
151
+ // a clean bill of health.
152
+ for (const occurrence of check.occurrences) {
153
+ const text = occurrence && typeof occurrence.summary === 'string' ? occurrence.summary : '';
154
+ if (!text) continue;
155
+ notices.push({
156
+ level: 'note',
157
+ message: { text },
158
+ associatedRule: { id: check.ruleId }
159
+ });
160
+ }
161
+ continue;
162
+ }
146
163
 
147
164
  for (const occurrence of check.occurrences) {
148
165
  if (!occurrence) continue;
@@ -179,7 +196,10 @@ function renderSarifReport(result, options = {}) {
179
196
  },
180
197
  // fail first: matches docs/REPORT.md's own "violations before advisory
181
198
  // findings" ordering.
182
- results: [...failResults, ...cantTellResults]
199
+ results: [...failResults, ...cantTellResults],
200
+ ...(notices.length
201
+ ? { invocations: [{ executionSuccessful: true, toolExecutionNotices: notices }] }
202
+ : {})
183
203
  }
184
204
  ]
185
205
  };