pour-engine 1.28.0 → 1.30.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/README.md +1 -6
- package/config/project.config.js +1 -1
- package/engine/index.js +18 -17
- package/engine/rules/best-practice/accesskeys.js +1 -0
- package/engine/rules/best-practice/button-type.js +1 -0
- package/engine/rules/best-practice/empty-heading.js +1 -0
- package/engine/rules/best-practice/fieldset-legend.js +1 -0
- package/engine/rules/best-practice/heading-order.js +1 -0
- package/engine/rules/best-practice/landmark-one-main.js +1 -0
- package/engine/rules/best-practice/landmark-top-level.js +1 -0
- package/engine/rules/best-practice/landmark-unique.js +1 -0
- package/engine/rules/best-practice/multiple-labels.js +1 -0
- package/engine/rules/best-practice/new-window-link.js +1 -0
- package/engine/rules/best-practice/no-autofocus.js +1 -0
- package/engine/rules/best-practice/page-heading-one.js +1 -0
- package/engine/rules/best-practice/positive-tabindex.js +1 -0
- package/engine/rules/best-practice/redundant-alt-phrase.js +1 -0
- package/engine/rules/best-practice/redundant-aria-label.js +1 -0
- package/engine/rules/best-practice/redundant-aria.js +1 -0
- package/engine/rules/best-practice/redundant-image-alt.js +1 -0
- package/engine/rules/best-practice/redundant-role.js +1 -0
- package/engine/rules/best-practice/redundant-tabindex.js +1 -0
- package/engine/rules/best-practice/region.js +1 -0
- package/engine/rules/wcag/1.1.1-area-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-canvas-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-embed-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-image-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-input-image-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-object-alt.js +1 -0
- package/engine/rules/wcag/1.1.1-svg-img-alt.js +1 -0
- package/engine/rules/wcag/1.2.2-media-captions.js +12 -5
- package/engine/rules/wcag/1.3.1-aria-required-children.js +1 -0
- package/engine/rules/wcag/1.3.1-aria-required-parent.js +1 -0
- package/engine/rules/wcag/1.3.1-definition-list.js +1 -0
- package/engine/rules/wcag/1.3.1-dlitem-parent.js +1 -0
- package/engine/rules/wcag/1.3.1-form-label.js +1 -0
- package/engine/rules/wcag/1.3.1-list-structure.js +1 -0
- package/engine/rules/wcag/1.3.1-listitem-parent.js +1 -0
- package/engine/rules/wcag/1.3.1-p-as-heading.js +1 -0
- package/engine/rules/wcag/1.3.1-table-headers.js +1 -0
- package/engine/rules/wcag/1.3.4-orientation.js +1 -0
- package/engine/rules/wcag/1.3.5-autocomplete-valid.js +1 -0
- package/engine/rules/wcag/1.4.1-link-in-text-block.js +1 -0
- package/engine/rules/wcag/1.4.10-reflow.js +1 -0
- package/engine/rules/wcag/1.4.11-non-text-contrast.js +1 -0
- package/engine/rules/wcag/1.4.12-text-spacing.js +1 -0
- package/engine/rules/wcag/1.4.2-audio-control.js +1 -0
- package/engine/rules/wcag/1.4.3-color-contrast.js +1 -0
- package/engine/rules/wcag/1.4.3-control-contrast.js +1 -0
- package/engine/rules/wcag/1.4.4-meta-viewport.js +1 -0
- package/engine/rules/wcag/1.4.6-contrast-enhanced.js +1 -0
- package/engine/rules/wcag/2.1.1-scrollable-region-focusable.js +1 -0
- package/engine/rules/wcag/2.2.1-meta-refresh.js +1 -0
- package/engine/rules/wcag/2.2.2-pause-stop-hide.js +1 -0
- package/engine/rules/wcag/2.2.2-video-loop-motion.js +1 -0
- package/engine/rules/wcag/2.4.1-bypass-blocks.js +1 -0
- package/engine/rules/wcag/2.4.11-focus-not-obscured.js +1 -0
- package/engine/rules/wcag/2.4.2-document-title.js +1 -0
- package/engine/rules/wcag/2.4.3-visual-order-divergence.js +1 -0
- package/engine/rules/wcag/2.4.4-link-name.js +1 -0
- package/engine/rules/wcag/2.4.4-link-text-generic.js +1 -0
- package/engine/rules/wcag/2.4.7-focus-visible.js +1 -0
- package/engine/rules/wcag/2.4.9-link-text-generic-only.js +1 -0
- package/engine/rules/wcag/2.5.3-label-in-name.js +1 -0
- package/engine/rules/wcag/2.5.5-target-size-enhanced.js +1 -0
- package/engine/rules/wcag/2.5.8-target-size.js +1 -0
- package/engine/rules/wcag/3.1.1-html-lang.js +1 -0
- package/engine/rules/wcag/3.1.2-valid-lang-parts.js +1 -0
- package/engine/rules/wcag/3.2.2-on-input-navigation.js +1 -0
- package/engine/rules/wcag/3.3.1-error-message-linkage.js +1 -0
- package/engine/rules/wcag/3.3.8-auth-field-obstruction.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-allowed-attr.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-attr-valid.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-field-name.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-hidden-focus.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-label-misuse.js +1 -0
- package/engine/rules/wcag/4.1.2-aria-valid-refs.js +117 -37
- package/engine/rules/wcag/4.1.2-button-name.js +1 -0
- package/engine/rules/wcag/4.1.2-composite-widget-name.js +1 -0
- package/engine/rules/wcag/4.1.2-dialog-name.js +1 -0
- package/engine/rules/wcag/4.1.2-frame-title.js +1 -0
- package/engine/rules/wcag/4.1.2-label-for-valid.js +1 -0
- package/engine/rules/wcag/4.1.2-nested-interactive.js +1 -0
- package/engine/rules/wcag/4.1.2-role-required-aria.js +1 -0
- package/engine/rules/wcag/4.1.2-summary-name.js +1 -0
- package/engine/rules/wcag/4.1.2-valid-role.js +1 -0
- package/package.json +1 -1
- package/engine/wcag30-draft.js +0 -149
package/README.md
CHANGED
|
@@ -21,10 +21,6 @@ can verify are returned as a `manualReview` checklist instead of being
|
|
|
21
21
|
silently skipped, and findings the engine can't judge conclusively land in
|
|
22
22
|
`incomplete` rather than being guessed at.
|
|
23
23
|
|
|
24
|
-
An experimental WCAG 3.0 draft mode (`engine/wcag30-draft.js`) reframes
|
|
25
|
-
results against the draft's guidelines — clearly stamped as a draft, because
|
|
26
|
-
no tool can honestly claim conformance to an unfinished standard.
|
|
27
|
-
|
|
28
24
|
## Getting started
|
|
29
25
|
|
|
30
26
|
```sh
|
|
@@ -39,7 +35,6 @@ const results = await run(document, {
|
|
|
39
35
|
// optional: filter rules by tag. Tags are cumulative — WCAG 2.2 AA means
|
|
40
36
|
// every A/AA rule from 2.0 through 2.2. Omit to run everything.
|
|
41
37
|
tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'],
|
|
42
|
-
// standard: 'wcag30-draft', // optional: experimental WCAG 3.0 draft mode
|
|
43
38
|
});
|
|
44
39
|
|
|
45
40
|
console.log(results.violations, results.passes, results.incomplete);
|
|
@@ -89,7 +84,7 @@ static HTML strings. For Node-based testing, drive it through a real browser
|
|
|
89
84
|
(`1.1.1-image-alt.js`), plus the catalog index.
|
|
90
85
|
- `engine/lib/` — shared helpers: DOM traversal, accessible-name
|
|
91
86
|
computation, contrast math, role model.
|
|
92
|
-
- `engine/wcag22.js
|
|
87
|
+
- `engine/wcag22.js` — the WCAG catalog.
|
|
93
88
|
- `config/project.config.js` — package-local stand-in for the pour
|
|
94
89
|
monorepo's project config (engine name + version only).
|
|
95
90
|
|
package/config/project.config.js
CHANGED
package/engine/index.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import config from '../config/project.config.js';
|
|
5
5
|
import rules from './rules/index.js';
|
|
6
6
|
import wcagCatalog from './wcag22.js';
|
|
7
|
-
import wcag30Draft from './wcag30-draft.js';
|
|
8
7
|
import { isVisible, isRendered, cssPath, htmlSnippet, ownText, collectRoots } from './lib/dom.js';
|
|
9
8
|
import { accessibleName } from './lib/accessible-name.js';
|
|
10
9
|
import { resetAuditCaches } from './lib/contrast.js';
|
|
@@ -79,16 +78,11 @@ function toResultNode(element, outcome) {
|
|
|
79
78
|
/**
|
|
80
79
|
* Run the audit.
|
|
81
80
|
* @param {Document|Element} context - what to scan
|
|
82
|
-
* @param {{ tags?: string[], exclude?: string,
|
|
81
|
+
* @param {{ tags?: string[], exclude?: string, signal?: AbortSignal }} options -
|
|
83
82
|
* rule selection by tag (wcag2a…wcag22aa, best-practice), empty selects
|
|
84
83
|
* every rule; exclude is a CSS selector — elements matching it, or inside
|
|
85
84
|
* a match (including across shadow boundaries), are left out of every
|
|
86
|
-
* rule;
|
|
87
|
-
* Working Draft (results.standard is stamped draft, the manual checklist
|
|
88
|
-
* becomes the draft guidelines, and each rule result carries the draft
|
|
89
|
-
* guideline it maps to) — the rules themselves stay WCAG 2.x, which is
|
|
90
|
-
* the only honest claim a tool can make about a draft;
|
|
91
|
-
* signal aborts the run between rules (throws AbortError)
|
|
85
|
+
* rule; signal aborts the run between rules (throws AbortError)
|
|
92
86
|
* @param {(progress: object) => void} [onProgress] - called before each rule
|
|
93
87
|
* starts ({rule, running}), during a rule at each renderer yield with a
|
|
94
88
|
* fractional done (completed rules + this rule's evaluated-element share,
|
|
@@ -97,17 +91,15 @@ function toResultNode(element, outcome) {
|
|
|
97
91
|
*/
|
|
98
92
|
export async function run(context = document, options = {}, onProgress) {
|
|
99
93
|
const auditStarted = performance.now();
|
|
100
|
-
const draft30 = options.standard === 'wcag30-draft';
|
|
101
94
|
const results = {
|
|
102
95
|
testEngine: { name, version },
|
|
103
96
|
timestamp: new Date().toISOString(),
|
|
104
97
|
url: context.location?.href ?? context.ownerDocument?.location?.href ?? '',
|
|
105
|
-
standard: draft30 ? wcag30Draft.draftInfo : null,
|
|
106
98
|
violations: [],
|
|
107
99
|
passes: [],
|
|
108
100
|
incomplete: [],
|
|
109
101
|
inapplicable: [],
|
|
110
|
-
manualReview:
|
|
102
|
+
manualReview: manualReviewCriteria(options.tags),
|
|
111
103
|
ruleTimings: [],
|
|
112
104
|
};
|
|
113
105
|
|
|
@@ -185,7 +177,9 @@ export async function run(context = document, options = {}, onProgress) {
|
|
|
185
177
|
|
|
186
178
|
let outcomes;
|
|
187
179
|
if (rule.evaluateAll) {
|
|
188
|
-
|
|
180
|
+
// Awaiting a plain array is a no-op, so sync evaluateAll rules are
|
|
181
|
+
// untouched — async ones (the deferred-reference probe) resolve here.
|
|
182
|
+
outcomes = await rule.evaluateAll(elements, ruleHelpers);
|
|
189
183
|
} else {
|
|
190
184
|
// Evaluate sequentially with periodic yields so ONE heavy rule on a
|
|
191
185
|
// huge element set can't freeze the page for its whole duration —
|
|
@@ -221,17 +215,15 @@ export async function run(context = document, options = {}, onProgress) {
|
|
|
221
215
|
|
|
222
216
|
const ruleResult = {
|
|
223
217
|
id: rule.id,
|
|
218
|
+
// Human display name ("Form field labels"); the id stays the
|
|
219
|
+
// machine handle everywhere (selectors, exports, docs anchors).
|
|
220
|
+
name: rule.name ?? rule.id,
|
|
224
221
|
impact: rule.impact,
|
|
225
222
|
tags: rule.tags,
|
|
226
223
|
help: rule.help,
|
|
227
224
|
description: rule.help,
|
|
228
225
|
helpUrl: rule.helpUrl,
|
|
229
226
|
};
|
|
230
|
-
if (draft30) {
|
|
231
|
-
// Which draft guideline this 2.x rule speaks to (null for rules with
|
|
232
|
-
// no draft counterpart, e.g. best-practice extras).
|
|
233
|
-
ruleResult.wcag30 = wcag30Draft.guidelineForTags(rule.tags);
|
|
234
|
-
}
|
|
235
227
|
if (!elements.length) results.inapplicable.push({ ...ruleResult, nodes: [] });
|
|
236
228
|
if (buckets.fail.length) results.violations.push({ ...ruleResult, nodes: buckets.fail });
|
|
237
229
|
if (buckets.incomplete.length) results.incomplete.push({ ...ruleResult, nodes: buckets.incomplete });
|
|
@@ -250,6 +242,15 @@ export async function run(context = document, options = {}, onProgress) {
|
|
|
250
242
|
ms,
|
|
251
243
|
counts: { ...liveCounts },
|
|
252
244
|
typeCounts: { ...liveTypeCounts },
|
|
245
|
+
// The completed rule's actual findings, so a UI can BUILD its list
|
|
246
|
+
// as rules finish rather than delivering everything at the end.
|
|
247
|
+
// Present only when the rule found something.
|
|
248
|
+
...(buckets.fail.length || buckets.incomplete.length ? {
|
|
249
|
+
findings: {
|
|
250
|
+
...(buckets.fail.length ? { violation: { ...ruleResult, nodes: buckets.fail } } : {}),
|
|
251
|
+
...(buckets.incomplete.length ? { incomplete: { ...ruleResult, nodes: buckets.incomplete } } : {}),
|
|
252
|
+
},
|
|
253
|
+
} : {}),
|
|
253
254
|
});
|
|
254
255
|
}
|
|
255
256
|
|
|
@@ -11,6 +11,7 @@ const NAME_FROM_CONTENT = new Set(['button', 'link', 'menuitem', 'menuitemcheckb
|
|
|
11
11
|
|
|
12
12
|
export default {
|
|
13
13
|
id: 'redundant-aria-label',
|
|
14
|
+
name: 'Redundant aria-label',
|
|
14
15
|
impact: 'minor',
|
|
15
16
|
tags: ['best-practice'],
|
|
16
17
|
help: 'aria-label should not repeat the visible text',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// WCAG SC 1.1.1 Non-text Content (Level A) · SC 2.4.4 Link Purpose (In Context) (Level A)
|
|
2
2
|
export default {
|
|
3
3
|
id: 'area-alt',
|
|
4
|
+
name: 'Image map alt text',
|
|
4
5
|
impact: 'critical',
|
|
5
6
|
tags: ['wcag2a', 'wcag111', 'wcag244'],
|
|
6
7
|
help: 'Image map areas must have alternative text',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// WCAG SC 1.1.1 Non-text Content (Level A) · SC 4.1.2 Name, Role, Value (Level A)
|
|
2
2
|
export default {
|
|
3
3
|
id: 'input-image-alt',
|
|
4
|
+
name: 'Image button alt text',
|
|
4
5
|
impact: 'critical',
|
|
5
6
|
tags: ['wcag2a', 'wcag111', 'wcag412'],
|
|
6
7
|
help: 'Image inputs need alt text describing their action',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// WCAG SC 1.2.2 Captions (Prerecorded) (Level A)
|
|
2
2
|
export default {
|
|
3
3
|
id: 'media-captions',
|
|
4
|
+
name: 'Video captions',
|
|
4
5
|
impact: 'critical',
|
|
5
6
|
tags: ['wcag2a', 'wcag122'],
|
|
6
7
|
help: 'Video content must have captions',
|
|
@@ -21,17 +22,23 @@ export default {
|
|
|
21
22
|
if (!element.currentSrc && !element.getAttribute('src') && !element.querySelector('source')) {
|
|
22
23
|
return { status: 'pass' };
|
|
23
24
|
}
|
|
24
|
-
// The decorative no-audio-path signature
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// the file may carry is never presented to anyone; audio that cannot be
|
|
25
|
+
// The decorative no-audio-path signature: muted with no controls. With
|
|
26
|
+
// no controls there is no user path to unmute, so whatever audio the
|
|
27
|
+
// file may carry is never presented to anyone; audio that cannot be
|
|
28
28
|
// presented is not audio content under 1.2.2, and asking a human to
|
|
29
29
|
// verify captions for it is noise (measured on the benchmark corpus:
|
|
30
30
|
// our own site's aria-hidden hero loops, ffprobe-confirmed to carry no
|
|
31
31
|
// audio stream, plus the same family on four news/university homes).
|
|
32
|
+
// HOW playback starts is deliberately not part of the signature: an
|
|
33
|
+
// earlier autoplay condition missed the two other members of the same
|
|
34
|
+
// family, measured on our own site — a crossfade B-roll take that only
|
|
35
|
+
// ever plays via script (no autoplay attribute, so the review fired
|
|
36
|
+
// exactly when the script had wired its src), and heroes whose
|
|
37
|
+
// autoplay attribute a reduced-motion handler removes. Muteness and
|
|
38
|
+
// controls decide whether audio can reach anyone; autoplay never did.
|
|
32
39
|
// Properties, not attributes: script-muted heroes count too. A muted
|
|
33
40
|
// video WITH controls keeps its review — one click unmutes it.
|
|
34
|
-
if (element.muted &&
|
|
41
|
+
if (element.muted && !element.controls) {
|
|
35
42
|
return { status: 'pass' };
|
|
36
43
|
}
|
|
37
44
|
// Captions may be burned in or provided by the player — a human must check.
|
|
@@ -20,6 +20,7 @@ const IMPLICIT_CONTAINER = { ul: 'list', ol: 'list', menu: 'list', table: 'table
|
|
|
20
20
|
|
|
21
21
|
export default {
|
|
22
22
|
id: 'aria-required-parent',
|
|
23
|
+
name: 'Required ARIA parent',
|
|
23
24
|
impact: 'critical',
|
|
24
25
|
tags: ['wcag2a', 'wcag131'],
|
|
25
26
|
help: 'ARIA child roles must be inside their required container role',
|
|
@@ -3,6 +3,7 @@ import { parseColor, contrastRatio } from '../../lib/contrast.js';
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
id: 'link-in-text-block',
|
|
6
|
+
name: 'Link distinction in text',
|
|
6
7
|
impact: 'serious',
|
|
7
8
|
tags: ['wcag2a', 'wcag141'],
|
|
8
9
|
help: 'Links inside text must be distinguishable by more than colour',
|
|
@@ -10,6 +10,7 @@ import { parseColor, contrastRatio, composite, effectiveBackground, showRatio }
|
|
|
10
10
|
|
|
11
11
|
export default {
|
|
12
12
|
id: 'non-text-contrast',
|
|
13
|
+
name: 'Control & icon contrast',
|
|
13
14
|
impact: 'serious',
|
|
14
15
|
tags: ['wcag21aa', 'wcag1411'],
|
|
15
16
|
help: 'Form fields and icons need a 3:1 visible boundary',
|
|
@@ -916,6 +916,7 @@ export function createContrastRule({ id, tags, help, helpUrl, thresholds }) {
|
|
|
916
916
|
|
|
917
917
|
export default createContrastRule({
|
|
918
918
|
id: 'color-contrast',
|
|
919
|
+
name: 'Text contrast',
|
|
919
920
|
tags: ['wcag2aa', 'wcag143'],
|
|
920
921
|
help: 'Text must have sufficient contrast against its background',
|
|
921
922
|
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html',
|
|
@@ -4,6 +4,7 @@ import { createContrastRule } from './1.4.3-color-contrast.js';
|
|
|
4
4
|
|
|
5
5
|
export default createContrastRule({
|
|
6
6
|
id: 'color-contrast-enhanced',
|
|
7
|
+
name: 'Enhanced text contrast',
|
|
7
8
|
tags: ['wcag2aaa', 'wcag146'],
|
|
8
9
|
help: 'Text must have enhanced contrast against its background (AAA)',
|
|
9
10
|
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/contrast-enhanced.html',
|
|
@@ -43,6 +43,7 @@ function hasTabbableContent(node) {
|
|
|
43
43
|
|
|
44
44
|
export default {
|
|
45
45
|
id: 'scrollable-region-focusable',
|
|
46
|
+
name: 'Keyboard-reachable scroll areas',
|
|
46
47
|
impact: 'serious',
|
|
47
48
|
tags: ['wcag2a', 'wcag211', 'wcag213'], // 2.1.3 is 2.1.1's AAA twin — same check satisfies both
|
|
48
49
|
help: 'Scrollable regions must be reachable by keyboard',
|