@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.
- package/CHANGELOG.md +240 -149
- package/README.md +51 -44
- package/docs/ACT_RULE_MAPPING.md +245 -0
- package/docs/API_STABILITY.md +53 -5
- package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
- package/docs/DESIGN_CHALLENGES.md +367 -0
- package/docs/EARL.md +100 -0
- package/docs/ENGINE_OPTIONS.md +42 -4
- package/docs/I18N.md +4 -4
- package/docs/INTEGRATION.md +4 -2
- package/docs/LIMITATIONS.md +9 -5
- package/docs/OUTPUT_SCHEMA.md +44 -6
- package/docs/POLICY.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_AUTHORING.md +63 -36
- package/docs/RULE_CATALOG.md +1928 -169
- package/docs/RULE_HELPERS.md +333 -0
- package/docs/RULE_TAXONOMY.md +27 -6
- package/docs/SARIF.md +21 -2
- package/docs/TROUBLESHOOTING.md +2 -2
- package/docs/WCAG_CONFORMANCE.md +34 -10
- package/package.json +11 -9
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +74 -10
- package/src/checks/automatic/aria-allowed-role.js +34 -25
- package/src/checks/automatic/aria-braille-equivalent.js +21 -13
- package/src/checks/automatic/aria-conditional-attr.js +22 -15
- package/src/checks/automatic/aria-deprecated-role.js +13 -1
- package/src/checks/automatic/aria-hidden-body.js +3 -3
- package/src/checks/automatic/aria-hidden-focus.js +5 -5
- package/src/checks/automatic/aria-prohibited-attr.js +23 -18
- package/src/checks/automatic/aria-prohibited-children.js +136 -43
- package/src/checks/automatic/aria-required-attr.js +119 -24
- package/src/checks/automatic/aria-required-children.js +54 -30
- package/src/checks/automatic/aria-required-parent.js +93 -15
- package/src/checks/automatic/aria-role-name-present.js +37 -23
- package/src/checks/automatic/aria-roles-valid.js +52 -21
- package/src/checks/automatic/aria-valid-attr-value.js +89 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -10
- package/src/checks/automatic/autocomplete-valid.js +2 -2
- package/src/checks/automatic/avoid-inline-spacing.js +133 -6
- package/src/checks/automatic/binary-control-name-present.js +27 -5
- package/src/checks/automatic/button-name-present.js +92 -6
- package/src/checks/automatic/combobox-name-present.js +26 -6
- package/src/checks/automatic/contrast-computable.js +42 -0
- package/src/checks/automatic/contrast-enhanced.js +33 -1
- package/src/checks/automatic/contrast-minimum.js +33 -1
- package/src/checks/automatic/css-orientation-lock.js +138 -24
- package/src/checks/automatic/definition-list-children-valid.js +7 -8
- package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
- package/src/checks/automatic/dialog-name-present.js +20 -2
- package/src/checks/automatic/duplicate-id-aria.js +10 -3
- package/src/checks/automatic/duplicate-id.js +203 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +10 -1
- package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
- package/src/checks/automatic/iframe-focusable-content.js +68 -7
- package/src/checks/automatic/iframe-name-present.js +37 -3
- package/src/checks/automatic/iframe-title-unique.js +1 -1
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +204 -68
- package/src/checks/automatic/link-in-text-block.js +285 -29
- package/src/checks/automatic/link-name-present.js +22 -1
- package/src/checks/automatic/list-children-valid.js +6 -6
- package/src/checks/automatic/listbox-name-present.js +28 -8
- package/src/checks/automatic/listitem-parent-valid.js +4 -4
- package/src/checks/automatic/menuitem-name-present.js +20 -2
- package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
- package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
- package/src/checks/automatic/meter-name-present.js +23 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
- package/src/checks/automatic/option-name-present.js +23 -4
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +23 -4
- package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
- package/src/checks/automatic/searchbox-name-present.js +28 -8
- package/src/checks/automatic/server-side-image-map-absent.js +1 -1
- package/src/checks/automatic/slider-name-present.js +27 -6
- package/src/checks/automatic/spinbutton-name-present.js +28 -8
- package/src/checks/automatic/summary-name-present.js +18 -2
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +21 -2
- package/src/checks/automatic/table-headers-attr-valid.js +43 -8
- package/src/checks/automatic/table-th-has-data-cells.js +61 -5
- package/src/checks/automatic/target-size-minimum.js +155 -58
- package/src/checks/automatic/td-has-header.js +24 -23
- package/src/checks/automatic/textbox-name-present.js +28 -8
- package/src/checks/automatic/tooltip-name-present.js +21 -2
- package/src/checks/automatic/treeitem-name-present.js +23 -4
- package/src/checks/automatic/valid-lang.js +92 -7
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +3 -3
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +58 -11
- package/src/checks/manual/empty-table-header-manual.js +8 -8
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +563 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
- package/src/checks/manual/heading-order-manual.js +3 -3
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +4 -4
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +4 -4
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
- package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
- package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
- package/src/checks/manual/landmark-one-main-manual.js +6 -6
- package/src/checks/manual/landmark-unique-manual.js +9 -9
- package/src/checks/manual/link-name-quality-manual.js +161 -32
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +2 -2
- package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +6 -6
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/password-paste-enabled-manual.js +255 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
- package/src/checks/manual/region-manual.js +19 -19
- package/src/checks/manual/scope-attr-valid-manual.js +2 -2
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +5 -5
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +2 -2
- package/src/checks/manual/table-duplicate-name-manual.js +2 -2
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +8880 -41883
- package/src/earl.js +144 -0
- package/src/report.js +2 -2
- package/src/sarif.js +22 -2
- package/surea11y.browser.js +10 -37882
- package/surea11y.i18n.de.js +2 -21
- package/surea11y.i18n.es.js +2 -21
- package/surea11y.i18n.fr.js +2 -21
- 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
|
|
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
|
|
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')
|
|
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
|
};
|