@surea11y/core 1.6.0 → 1.8.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 (120) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +179 -90
  3. package/docs/ACT_RULE_MAPPING.md +10 -8
  4. package/docs/API_STABILITY.md +67 -6
  5. package/docs/BINDING_AUTHORS_GUIDE.md +104 -2
  6. package/docs/CI_INTEGRATIONS.md +43 -0
  7. package/docs/DESIGN_CHALLENGES.md +162 -2
  8. package/docs/EARL.md +100 -0
  9. package/docs/ENGINE_OPTIONS.md +109 -5
  10. package/docs/I18N.md +62 -20
  11. package/docs/INTEGRATION.md +4 -2
  12. package/docs/JUNIT.md +73 -0
  13. package/docs/LIMITATIONS.md +4 -1
  14. package/docs/OUTPUT_SCHEMA.md +62 -11
  15. package/docs/POLICY.md +1 -1
  16. package/docs/REPORT.md +7 -2
  17. package/docs/RULE_AUTHORING.md +83 -17
  18. package/docs/RULE_CATALOG.md +212 -139
  19. package/docs/RULE_EXAMPLES.md +2189 -0
  20. package/docs/RULE_HELPERS.md +390 -0
  21. package/docs/RULE_TAXONOMY.md +27 -6
  22. package/docs/SARIF.md +23 -3
  23. package/docs/WCAG_CONFORMANCE.md +64 -3
  24. package/package.json +41 -12
  25. package/profiles/index.js +14 -0
  26. package/src/checks/automatic/area-alt-present.js +87 -31
  27. package/src/checks/automatic/aria-allowed-attr.js +6 -0
  28. package/src/checks/automatic/aria-allowed-role.js +32 -23
  29. package/src/checks/automatic/aria-braille-equivalent.js +43 -17
  30. package/src/checks/automatic/aria-conditional-attr.js +17 -10
  31. package/src/checks/automatic/aria-deprecated-role.js +12 -0
  32. package/src/checks/automatic/aria-hidden-body.js +1 -1
  33. package/src/checks/automatic/aria-hidden-focus.js +74 -18
  34. package/src/checks/automatic/aria-prohibited-attr.js +22 -4
  35. package/src/checks/automatic/aria-prohibited-children.js +6 -6
  36. package/src/checks/automatic/aria-required-attr.js +88 -12
  37. package/src/checks/automatic/aria-required-children.js +33 -16
  38. package/src/checks/automatic/aria-required-parent.js +32 -6
  39. package/src/checks/automatic/aria-role-name-present.js +20 -3
  40. package/src/checks/automatic/aria-roles-valid.js +52 -21
  41. package/src/checks/automatic/aria-valid-attr-value.js +89 -24
  42. package/src/checks/automatic/aria-valid-attr.js +14 -9
  43. package/src/checks/automatic/autocomplete-valid.js +152 -26
  44. package/src/checks/automatic/avoid-inline-spacing.js +207 -15
  45. package/src/checks/automatic/button-name-present.js +2 -1
  46. package/src/checks/automatic/canvas-text-alternative-present.js +105 -14
  47. package/src/checks/automatic/combobox-name-present.js +34 -51
  48. package/src/checks/automatic/contrast-computable.js +45 -4
  49. package/src/checks/automatic/contrast-enhanced.js +16 -4
  50. package/src/checks/automatic/contrast-minimum.js +57 -11
  51. package/src/checks/automatic/css-orientation-lock.js +171 -12
  52. package/src/checks/automatic/definition-list-children-valid.js +67 -23
  53. package/src/checks/automatic/deprecated-elements-not-used.js +43 -38
  54. package/src/checks/automatic/dialog-name-present.js +28 -9
  55. package/src/checks/automatic/duplicate-id-aria.js +5 -0
  56. package/src/checks/automatic/duplicate-id.js +19 -10
  57. package/src/checks/automatic/form-control-single-label.js +9 -0
  58. package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
  59. package/src/checks/automatic/iframe-focusable-content.js +12 -4
  60. package/src/checks/automatic/iframe-title-unique.js +36 -81
  61. package/src/checks/automatic/input-image-alt-present.js +32 -20
  62. package/src/checks/automatic/label-in-name.js +78 -69
  63. package/src/checks/automatic/language-page-present.js +12 -6
  64. package/src/checks/automatic/link-in-text-block.js +512 -44
  65. package/src/checks/automatic/link-name-present.js +13 -5
  66. package/src/checks/automatic/list-children-valid.js +18 -1
  67. package/src/checks/automatic/listbox-name-present.js +19 -49
  68. package/src/checks/automatic/listitem-parent-valid.js +4 -3
  69. package/src/checks/automatic/meta-refresh-no-exceptions.js +3 -3
  70. package/src/checks/automatic/page-title-present.js +16 -4
  71. package/src/checks/automatic/progressbar-name-present.js +11 -1
  72. package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +9 -5
  73. package/src/checks/automatic/searchbox-name-present.js +32 -49
  74. package/src/checks/automatic/server-side-image-map-absent.js +48 -28
  75. package/src/checks/automatic/slider-name-present.js +38 -52
  76. package/src/checks/automatic/spinbutton-name-present.js +32 -49
  77. package/src/checks/automatic/target-size-minimum.js +84 -16
  78. package/src/checks/automatic/td-has-header.js +60 -23
  79. package/src/checks/automatic/text-spacing-content-loss.js +548 -0
  80. package/src/checks/automatic/textbox-name-present.js +32 -49
  81. package/src/checks/automatic/valid-lang.js +15 -10
  82. package/src/checks/manual/area-alt-quality-manual.js +113 -31
  83. package/src/checks/manual/canvas-text-alternative-quality-manual.js +0 -2
  84. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +23 -10
  85. package/src/checks/manual/css-hidden-focus.js +215 -7
  86. package/src/checks/manual/form-control-label-quality-manual.js +243 -29
  87. package/src/checks/manual/heading-order-manual.js +9 -1
  88. package/src/checks/manual/heading-quality-manual.js +143 -9
  89. package/src/checks/manual/img-alt-decorative-manual.js +6 -3
  90. package/src/checks/manual/input-image-alt-quality-manual.js +81 -13
  91. package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
  92. package/src/checks/manual/link-name-quality-manual.js +130 -4
  93. package/src/checks/manual/media-transcript-present-manual.js +65 -8
  94. package/src/checks/manual/mouse-only-event-handlers-manual.js +66 -5
  95. package/src/checks/manual/no-autoplay-audio-manual.js +93 -6
  96. package/src/checks/manual/p-as-heading-manual.js +89 -44
  97. package/src/checks/manual/page-title-patterns-manual.js +77 -8
  98. package/src/checks/manual/password-paste-enabled-manual.js +255 -0
  99. package/src/checks/manual/skip-link-manual.js +42 -14
  100. package/src/checks/manual/table-fake-caption-manual.js +32 -1
  101. package/src/checks/manual/video-caption-manual.js +47 -24
  102. package/src/checks/manual-review.js +0 -4
  103. package/src/core.js +18061 -46194
  104. package/src/coverage/en301549-map.js +187 -0
  105. package/src/coverage/standards.js +279 -0
  106. package/src/coverage/wcag-facets.js +1119 -0
  107. package/src/coverage/wcag-version-map.js +101 -0
  108. package/src/earl.js +144 -0
  109. package/src/en301549.js +33 -0
  110. package/src/junit.js +321 -0
  111. package/src/profile-kit.js +163 -0
  112. package/src/report.js +343 -74
  113. package/src/sarif.js +56 -5
  114. package/src/wcag.js +105 -0
  115. package/surea11y.browser.js +11 -41039
  116. package/surea11y.i18n.de.js +2 -21
  117. package/surea11y.i18n.es.js +2 -21
  118. package/surea11y.i18n.fr.js +2 -21
  119. package/surea11y.i18n.ja.js +3 -0
  120. package/src/checks/manual/area-alt-decorative-manual.js +0 -255
package/src/sarif.js CHANGED
@@ -26,6 +26,7 @@
26
26
 
27
27
  const path = require('path');
28
28
  const { computeBaselineKey, getReasonCode } = require('./baseline.js');
29
+ const { standardOfEntry } = require('./coverage/standards.js');
29
30
 
30
31
  const SARIF_SCHEMA_URI =
31
32
  'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/Schemata/sarif-schema-2.1.0.json';
@@ -60,11 +61,27 @@ function buildRemainingBaselineMap(baselineEntries) {
60
61
  return remaining;
61
62
  }
62
63
 
63
- function wcagTags(check) {
64
+ // `normativeMappings` also carries other standards (EN 301 549 clauses, say) and
65
+ // WCAG's own non-normative documents (`type: 'Understanding'`), each with a
66
+ // `requirement` of its own. Only a WCAG Success Criterion earns a `wcag-` tag;
67
+ // an entry naming no standard is treated as WCAG, the engine's default.
68
+ function isWcagCriterion(m) {
69
+ return !!(m && m.requirement && (m.standard == null || m.standard === 'WCAG') && !m.type);
70
+ }
71
+
72
+ function ruleTags(check) {
64
73
  const mappings = (check.meta && check.meta.normativeMappings) || [];
65
74
  const tags = new Set(['accessibility', check.type === 'automatic' ? 'automatic' : 'manual']);
66
75
  for (const m of mappings) {
67
- if (m && m.requirement) tags.add(`wcag-${m.requirement}`);
76
+ if (isWcagCriterion(m)) tags.add(`wcag-${m.requirement}`);
77
+ }
78
+ // Each registered standard's entry gets a tag prefixed with its key
79
+ // (src/coverage/standards.js). The tag carries no version: EN 301 549 numbers
80
+ // a clause the same way in every version that has it, so two versions
81
+ // collapse into one tag.
82
+ for (const m of mappings) {
83
+ const standard = standardOfEntry(m);
84
+ if (standard) tags.add(`${standard.key}-${m.requirement}`);
68
85
  }
69
86
  return Array.from(tags);
70
87
  }
@@ -79,7 +96,7 @@ function buildRule(check) {
79
96
  // worst-case, rule-level default is "warning"; automatic rules can
80
97
  // reach "error" -- see docs/OUTPUT_SCHEMA.md's outcome/type table.
81
98
  defaultConfiguration: { level: check.type === 'automatic' ? 'error' : 'warning' },
82
- properties: { tags: wcagTags(check) }
99
+ properties: { tags: ruleTags(check) }
83
100
  };
84
101
  }
85
102
 
@@ -124,6 +141,19 @@ function getOccurrenceOutcome(check, occurrence) {
124
141
  return check && (check.outcome === 'fail' || check.outcome === 'cantTell') ? check.outcome : null;
125
142
  }
126
143
 
144
+ // The conformance target a run used, so a dashboard can tell a WCAG 2.1 run
145
+ // from a 2.2 one, and the opt-in rules it added beyond that target. Absent on
146
+ // results from engines that predate the fields.
147
+ function runProperties(result) {
148
+ const engine = (result && result.engine) || {};
149
+ const props = {};
150
+ if (engine.wcagVersion) props.wcagVersion = engine.wcagVersion;
151
+ if (engine.profile) props.profile = engine.profile;
152
+ if (Array.isArray(engine.optInRules) && engine.optInRules.length)
153
+ props.optInRules = engine.optInRules.slice();
154
+ return Object.keys(props).length ? props : null;
155
+ }
156
+
127
157
  function renderSarifReport(result, options = {}) {
128
158
  const { toolVersion, informationUri, baselineEntries } = options;
129
159
  const artifactUri = artifactUriFromResult(result);
@@ -133,6 +163,7 @@ function renderSarifReport(result, options = {}) {
133
163
  const seenRuleIds = new Set();
134
164
  const failResults = [];
135
165
  const cantTellResults = [];
166
+ const notices = [];
136
167
 
137
168
  for (const check of (result && result.checksResults) || []) {
138
169
  if (!check || !Array.isArray(check.occurrences)) continue;
@@ -142,7 +173,23 @@ function renderSarifReport(result, options = {}) {
142
173
  rules.push(buildRule(check));
143
174
  }
144
175
 
145
- if (check.outcome !== 'fail' && check.outcome !== 'cantTell') continue;
176
+ if (check.outcome !== 'fail' && check.outcome !== 'cantTell') {
177
+ // A rule with nothing to judge may still say why, which is the
178
+ // difference between "checked, nothing to flag" and "could not check".
179
+ // That is not an alert, so it cannot be a result; carrying it as an
180
+ // execution notice keeps a SARIF-only pipeline from reading silence as
181
+ // a clean bill of health.
182
+ for (const occurrence of check.occurrences) {
183
+ const text = occurrence && typeof occurrence.summary === 'string' ? occurrence.summary : '';
184
+ if (!text) continue;
185
+ notices.push({
186
+ level: 'note',
187
+ message: { text },
188
+ associatedRule: { id: check.ruleId }
189
+ });
190
+ }
191
+ continue;
192
+ }
146
193
 
147
194
  for (const occurrence of check.occurrences) {
148
195
  if (!occurrence) continue;
@@ -179,7 +226,11 @@ function renderSarifReport(result, options = {}) {
179
226
  },
180
227
  // fail first: matches docs/REPORT.md's own "violations before advisory
181
228
  // findings" ordering.
182
- results: [...failResults, ...cantTellResults]
229
+ results: [...failResults, ...cantTellResults],
230
+ ...(runProperties(result) ? { properties: runProperties(result) } : {}),
231
+ ...(notices.length
232
+ ? { invocations: [{ executionSuccessful: true, toolExecutionNotices: notices }] }
233
+ : {})
183
234
  }
184
235
  ]
185
236
  };
package/src/wcag.js ADDED
@@ -0,0 +1,105 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
3
+ 'use strict';
4
+
5
+ /**
6
+ * WCAG's Success Criteria as each version of WCAG 2 publishes them: number,
7
+ * title and level, the criteria in force in that version only. Published for
8
+ * tools and for standards built on a WCAG version (a profile's tables, such
9
+ * as a standard restating WCAG A and AA), so they read the version they are
10
+ * built on instead of keeping their own copy (docs/WCAG_CONFORMANCE.md).
11
+ *
12
+ * The engine's own tables describe WCAG 2.2, where 4.1.1 Parsing is
13
+ * "Obsolete and removed" and has no level. In 2.0 and 2.1 it was a Level A
14
+ * criterion titled "Parsing", and that is what wcagCriteria('2.1') returns.
15
+ *
16
+ * wcagTags() gives the engine's rule tags for a WCAG version and levels, the
17
+ * tag set a conformance profile on that version selects rules by
18
+ * (docs/ENGINE_OPTIONS.md).
19
+ *
20
+ * The returned objects are frozen.
21
+ */
22
+
23
+ const { FACETS } = require('./coverage/wcag-facets.js');
24
+ const { introducedInVersion, removedInVersion } = require('./coverage/wcag-version-map.js');
25
+
26
+ const WCAG_VERSIONS = Object.freeze(['2.0', '2.1', '2.2']);
27
+ const LEVELS = ['A', 'AA', 'AAA'];
28
+
29
+ // How a removed criterion read in the versions before its removal.
30
+ const BEFORE_REMOVAL = { '4.1.1': { title: 'Parsing', level: 'A' } };
31
+
32
+ function compareSc(a, b) {
33
+ const pa = a.split('.').map(Number);
34
+ const pb = b.split('.').map(Number);
35
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
36
+ return 0;
37
+ }
38
+
39
+ function checkVersion(version) {
40
+ if (!WCAG_VERSIONS.includes(version)) {
41
+ throw new Error(`unknown WCAG version "${version}"; one of ${WCAG_VERSIONS.join(', ')}`);
42
+ }
43
+ }
44
+
45
+ const cache = new Map();
46
+
47
+ // The criteria in force in a WCAG version, in numeric order:
48
+ // [{ sc, title, level, introduced }]. `levels` keeps only those levels
49
+ // (['A', 'AA'] for an A and AA target).
50
+ function wcagCriteria(version, { levels } = {}) {
51
+ checkVersion(version);
52
+ if (levels !== undefined) {
53
+ const bad = [].concat(levels).filter((l) => !LEVELS.includes(l));
54
+ if (bad.length) throw new Error(`unknown WCAG level ${bad.join(', ')}; one of A, AA, AAA`);
55
+ }
56
+ if (!cache.has(version)) {
57
+ const at = WCAG_VERSIONS.indexOf(version);
58
+ const list = Object.keys(FACETS)
59
+ .filter((sc) => WCAG_VERSIONS.indexOf(introducedInVersion(sc)) <= at)
60
+ .filter((sc) => {
61
+ const removed = removedInVersion(sc);
62
+ return !(removed && WCAG_VERSIONS.indexOf(removed) <= at);
63
+ })
64
+ .sort(compareSc)
65
+ .map((sc) => {
66
+ const own = removedInVersion(sc) ? BEFORE_REMOVAL[sc] || {} : {};
67
+ return Object.freeze({
68
+ sc,
69
+ title: own.title || FACETS[sc].title,
70
+ level: own.level || FACETS[sc].level,
71
+ introduced: introducedInVersion(sc)
72
+ });
73
+ });
74
+ cache.set(version, Object.freeze(list));
75
+ }
76
+ const all = cache.get(version);
77
+ if (levels === undefined) return all;
78
+ const keep = [].concat(levels);
79
+ return Object.freeze(all.filter((c) => keep.includes(c.level)));
80
+ }
81
+
82
+ // The tag each version adds its criteria under: wcag2a for 2.0's Level A,
83
+ // wcag21aa for the AA criteria 2.1 introduced, and so on.
84
+ const TAG_PREFIX = { '2.0': 'wcag2', 2.1: 'wcag21', 2.2: 'wcag22' };
85
+
86
+ // The rule tags that select a WCAG version's criteria at the given levels
87
+ // (A and AA by default): those of every version up to it, oldest first.
88
+ // ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] for 2.1 A and AA.
89
+ function wcagTags(version, levels = ['A', 'AA']) {
90
+ checkVersion(version);
91
+ const keep = [].concat(levels);
92
+ const bad = keep.filter((l) => !LEVELS.includes(l));
93
+ if (bad.length) throw new Error(`unknown WCAG level ${bad.join(', ')}; one of A, AA, AAA`);
94
+ return WCAG_VERSIONS.slice(0, WCAG_VERSIONS.indexOf(version) + 1).flatMap((v) =>
95
+ LEVELS.filter((l) => keep.includes(l)).map((l) => TAG_PREFIX[v] + l.toLowerCase())
96
+ );
97
+ }
98
+
99
+ // One criterion as a WCAG version publishes it, or null when the version has
100
+ // no such criterion.
101
+ function wcagCriterion(sc, version) {
102
+ return wcagCriteria(version).find((c) => c.sc === String(sc)) || null;
103
+ }
104
+
105
+ module.exports = { WCAG_VERSIONS, wcagCriteria, wcagCriterion, wcagTags };