@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.
- package/CHANGELOG.md +140 -0
- package/README.md +179 -90
- package/docs/ACT_RULE_MAPPING.md +10 -8
- package/docs/API_STABILITY.md +67 -6
- package/docs/BINDING_AUTHORS_GUIDE.md +104 -2
- package/docs/CI_INTEGRATIONS.md +43 -0
- package/docs/DESIGN_CHALLENGES.md +162 -2
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +109 -5
- package/docs/I18N.md +62 -20
- package/docs/INTEGRATION.md +4 -2
- package/docs/JUNIT.md +73 -0
- package/docs/LIMITATIONS.md +4 -1
- package/docs/OUTPUT_SCHEMA.md +62 -11
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +7 -2
- package/docs/RULE_AUTHORING.md +83 -17
- package/docs/RULE_CATALOG.md +212 -139
- package/docs/RULE_EXAMPLES.md +2189 -0
- package/docs/RULE_HELPERS.md +390 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +23 -3
- package/docs/WCAG_CONFORMANCE.md +64 -3
- package/package.json +41 -12
- package/profiles/index.js +14 -0
- package/src/checks/automatic/area-alt-present.js +87 -31
- package/src/checks/automatic/aria-allowed-attr.js +6 -0
- package/src/checks/automatic/aria-allowed-role.js +32 -23
- package/src/checks/automatic/aria-braille-equivalent.js +43 -17
- package/src/checks/automatic/aria-conditional-attr.js +17 -10
- package/src/checks/automatic/aria-deprecated-role.js +12 -0
- package/src/checks/automatic/aria-hidden-body.js +1 -1
- package/src/checks/automatic/aria-hidden-focus.js +74 -18
- package/src/checks/automatic/aria-prohibited-attr.js +22 -4
- package/src/checks/automatic/aria-prohibited-children.js +6 -6
- package/src/checks/automatic/aria-required-attr.js +88 -12
- package/src/checks/automatic/aria-required-children.js +33 -16
- package/src/checks/automatic/aria-required-parent.js +32 -6
- package/src/checks/automatic/aria-role-name-present.js +20 -3
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -24
- package/src/checks/automatic/aria-valid-attr.js +14 -9
- package/src/checks/automatic/autocomplete-valid.js +152 -26
- package/src/checks/automatic/avoid-inline-spacing.js +207 -15
- package/src/checks/automatic/button-name-present.js +2 -1
- package/src/checks/automatic/canvas-text-alternative-present.js +105 -14
- package/src/checks/automatic/combobox-name-present.js +34 -51
- package/src/checks/automatic/contrast-computable.js +45 -4
- package/src/checks/automatic/contrast-enhanced.js +16 -4
- package/src/checks/automatic/contrast-minimum.js +57 -11
- package/src/checks/automatic/css-orientation-lock.js +171 -12
- package/src/checks/automatic/definition-list-children-valid.js +67 -23
- package/src/checks/automatic/deprecated-elements-not-used.js +43 -38
- package/src/checks/automatic/dialog-name-present.js +28 -9
- package/src/checks/automatic/duplicate-id-aria.js +5 -0
- package/src/checks/automatic/duplicate-id.js +19 -10
- package/src/checks/automatic/form-control-single-label.js +9 -0
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +12 -4
- package/src/checks/automatic/iframe-title-unique.js +36 -81
- package/src/checks/automatic/input-image-alt-present.js +32 -20
- package/src/checks/automatic/label-in-name.js +78 -69
- package/src/checks/automatic/language-page-present.js +12 -6
- package/src/checks/automatic/link-in-text-block.js +512 -44
- package/src/checks/automatic/link-name-present.js +13 -5
- package/src/checks/automatic/list-children-valid.js +18 -1
- package/src/checks/automatic/listbox-name-present.js +19 -49
- package/src/checks/automatic/listitem-parent-valid.js +4 -3
- package/src/checks/automatic/meta-refresh-no-exceptions.js +3 -3
- package/src/checks/automatic/page-title-present.js +16 -4
- package/src/checks/automatic/progressbar-name-present.js +11 -1
- package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +9 -5
- package/src/checks/automatic/searchbox-name-present.js +32 -49
- package/src/checks/automatic/server-side-image-map-absent.js +48 -28
- package/src/checks/automatic/slider-name-present.js +38 -52
- package/src/checks/automatic/spinbutton-name-present.js +32 -49
- package/src/checks/automatic/target-size-minimum.js +84 -16
- package/src/checks/automatic/td-has-header.js +60 -23
- package/src/checks/automatic/text-spacing-content-loss.js +548 -0
- package/src/checks/automatic/textbox-name-present.js +32 -49
- package/src/checks/automatic/valid-lang.js +15 -10
- package/src/checks/manual/area-alt-quality-manual.js +113 -31
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +0 -2
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +23 -10
- package/src/checks/manual/css-hidden-focus.js +215 -7
- package/src/checks/manual/form-control-label-quality-manual.js +243 -29
- package/src/checks/manual/heading-order-manual.js +9 -1
- package/src/checks/manual/heading-quality-manual.js +143 -9
- package/src/checks/manual/img-alt-decorative-manual.js +6 -3
- package/src/checks/manual/input-image-alt-quality-manual.js +81 -13
- package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
- package/src/checks/manual/link-name-quality-manual.js +130 -4
- package/src/checks/manual/media-transcript-present-manual.js +65 -8
- package/src/checks/manual/mouse-only-event-handlers-manual.js +66 -5
- package/src/checks/manual/no-autoplay-audio-manual.js +93 -6
- package/src/checks/manual/p-as-heading-manual.js +89 -44
- package/src/checks/manual/page-title-patterns-manual.js +77 -8
- package/src/checks/manual/password-paste-enabled-manual.js +255 -0
- package/src/checks/manual/skip-link-manual.js +42 -14
- package/src/checks/manual/table-fake-caption-manual.js +32 -1
- package/src/checks/manual/video-caption-manual.js +47 -24
- package/src/checks/manual-review.js +0 -4
- package/src/core.js +18061 -46194
- package/src/coverage/en301549-map.js +187 -0
- package/src/coverage/standards.js +279 -0
- package/src/coverage/wcag-facets.js +1119 -0
- package/src/coverage/wcag-version-map.js +101 -0
- package/src/earl.js +144 -0
- package/src/en301549.js +33 -0
- package/src/junit.js +321 -0
- package/src/profile-kit.js +163 -0
- package/src/report.js +343 -74
- package/src/sarif.js +56 -5
- package/src/wcag.js +105 -0
- package/surea11y.browser.js +11 -41039
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- package/surea11y.i18n.ja.js +3 -0
- 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
|
-
|
|
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
|
|
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:
|
|
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')
|
|
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 };
|