@surea11y/core 1.7.0 → 1.8.1

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 (103) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.md +157 -54
  3. package/docs/ACT_RULE_MAPPING.md +8 -7
  4. package/docs/API_STABILITY.md +18 -5
  5. package/docs/BINDING_AUTHORS_GUIDE.md +2 -2
  6. package/docs/CI_INTEGRATIONS.md +43 -0
  7. package/docs/DESIGN_CHALLENGES.md +97 -3
  8. package/docs/EARL.md +2 -2
  9. package/docs/ENGINE_OPTIONS.md +81 -3
  10. package/docs/I18N.md +62 -20
  11. package/docs/JUNIT.md +73 -0
  12. package/docs/LIMITATIONS.md +1 -0
  13. package/docs/OUTPUT_SCHEMA.md +19 -6
  14. package/docs/REPORT.md +7 -2
  15. package/docs/RULE_AUTHORING.md +73 -6
  16. package/docs/RULE_CATALOG.md +139 -116
  17. package/docs/RULE_EXAMPLES.md +2189 -0
  18. package/docs/RULE_HELPERS.md +62 -5
  19. package/docs/RULE_TAXONOMY.md +2 -2
  20. package/docs/SARIF.md +2 -1
  21. package/docs/WCAG_CONFORMANCE.md +56 -3
  22. package/package.json +34 -11
  23. package/profiles/index.js +14 -0
  24. package/src/checks/automatic/area-alt-present.js +87 -31
  25. package/src/checks/automatic/aria-braille-equivalent.js +25 -7
  26. package/src/checks/automatic/aria-hidden-focus.js +74 -18
  27. package/src/checks/automatic/aria-prohibited-attr.js +17 -4
  28. package/src/checks/automatic/aria-required-attr.js +29 -0
  29. package/src/checks/automatic/aria-role-name-present.js +19 -2
  30. package/src/checks/automatic/aria-valid-attr-value.js +28 -16
  31. package/src/checks/automatic/autocomplete-valid.js +26 -11
  32. package/src/checks/automatic/avoid-inline-spacing.js +105 -40
  33. package/src/checks/automatic/button-name-present.js +2 -1
  34. package/src/checks/automatic/canvas-text-alternative-present.js +105 -14
  35. package/src/checks/automatic/combobox-name-present.js +34 -51
  36. package/src/checks/automatic/contrast-computable.js +35 -4
  37. package/src/checks/automatic/contrast-enhanced.js +4 -4
  38. package/src/checks/automatic/contrast-minimum.js +45 -11
  39. package/src/checks/automatic/css-orientation-lock.js +152 -30
  40. package/src/checks/automatic/definition-list-children-valid.js +67 -23
  41. package/src/checks/automatic/deprecated-elements-not-used.js +43 -38
  42. package/src/checks/automatic/dialog-name-present.js +28 -9
  43. package/src/checks/automatic/duplicate-id.js +6 -2
  44. package/src/checks/automatic/identical-iframes-same-purpose.js +4 -4
  45. package/src/checks/automatic/iframe-focusable-content.js +7 -4
  46. package/src/checks/automatic/iframe-title-unique.js +36 -81
  47. package/src/checks/automatic/input-image-alt-present.js +32 -20
  48. package/src/checks/automatic/label-in-name.js +40 -13
  49. package/src/checks/automatic/language-page-present.js +12 -6
  50. package/src/checks/automatic/link-in-text-block.js +272 -60
  51. package/src/checks/automatic/link-name-present.js +13 -5
  52. package/src/checks/automatic/list-children-valid.js +18 -1
  53. package/src/checks/automatic/listbox-name-present.js +19 -49
  54. package/src/checks/automatic/listitem-parent-valid.js +4 -3
  55. package/src/checks/automatic/meta-refresh-no-exceptions.js +3 -3
  56. package/src/checks/automatic/page-title-present.js +16 -4
  57. package/src/checks/automatic/progressbar-name-present.js +11 -1
  58. package/src/checks/automatic/role-img-text-alternative-present.js +9 -5
  59. package/src/checks/automatic/searchbox-name-present.js +32 -49
  60. package/src/checks/automatic/server-side-image-map-absent.js +48 -28
  61. package/src/checks/automatic/slider-name-present.js +38 -52
  62. package/src/checks/automatic/spinbutton-name-present.js +32 -49
  63. package/src/checks/automatic/target-size-minimum.js +0 -11
  64. package/src/checks/automatic/td-has-header.js +41 -5
  65. package/src/checks/automatic/text-spacing-content-loss.js +548 -0
  66. package/src/checks/automatic/textbox-name-present.js +32 -49
  67. package/src/checks/automatic/valid-lang.js +15 -10
  68. package/src/checks/manual/area-alt-quality-manual.js +113 -31
  69. package/src/checks/manual/canvas-text-alternative-quality-manual.js +0 -2
  70. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +23 -10
  71. package/src/checks/manual/css-hidden-focus.js +215 -7
  72. package/src/checks/manual/form-control-label-quality-manual.js +109 -5
  73. package/src/checks/manual/heading-order-manual.js +9 -1
  74. package/src/checks/manual/heading-quality-manual.js +143 -9
  75. package/src/checks/manual/img-alt-decorative-manual.js +6 -3
  76. package/src/checks/manual/input-image-alt-quality-manual.js +81 -13
  77. package/src/checks/manual/link-name-quality-manual.js +130 -4
  78. package/src/checks/manual/media-transcript-present-manual.js +65 -8
  79. package/src/checks/manual/mouse-only-event-handlers-manual.js +66 -5
  80. package/src/checks/manual/no-autoplay-audio-manual.js +93 -6
  81. package/src/checks/manual/p-as-heading-manual.js +89 -44
  82. package/src/checks/manual/page-title-patterns-manual.js +77 -8
  83. package/src/checks/manual/skip-link-manual.js +42 -14
  84. package/src/checks/manual/table-fake-caption-manual.js +32 -1
  85. package/src/checks/manual/video-caption-manual.js +47 -24
  86. package/src/checks/manual-review.js +0 -4
  87. package/src/core.js +14285 -2219
  88. package/src/coverage/en301549-map.js +187 -0
  89. package/src/coverage/standards.js +279 -0
  90. package/src/coverage/wcag-facets.js +1119 -0
  91. package/src/coverage/wcag-version-map.js +101 -0
  92. package/src/en301549.js +33 -0
  93. package/src/junit.js +321 -0
  94. package/src/profile-kit.js +163 -0
  95. package/src/report.js +343 -74
  96. package/src/sarif.js +34 -3
  97. package/src/wcag.js +105 -0
  98. package/surea11y.browser.js +5 -4
  99. package/surea11y.i18n.de.js +1 -1
  100. package/surea11y.i18n.es.js +1 -1
  101. package/surea11y.i18n.fr.js +1 -1
  102. package/surea11y.i18n.ja.js +3 -0
  103. 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);
@@ -197,6 +227,7 @@ function renderSarifReport(result, options = {}) {
197
227
  // fail first: matches docs/REPORT.md's own "violations before advisory
198
228
  // findings" ordering.
199
229
  results: [...failResults, ...cantTellResults],
230
+ ...(runProperties(result) ? { properties: runProperties(result) } : {}),
200
231
  ...(notices.length
201
232
  ? { invocations: [{ executionSuccessful: true, toolExecutionNotices: notices }] }
202
233
  : {})
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 };