pour-engine 1.1.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/LICENSE +21 -0
- package/README.md +99 -0
- package/config/project.config.js +11 -0
- package/engine/COVERAGE.md +115 -0
- package/engine/README.md +115 -0
- package/engine/index.js +258 -0
- package/engine/lib/accessible-name.js +126 -0
- package/engine/lib/contrast.js +628 -0
- package/engine/lib/dom.js +116 -0
- package/engine/lib/roles.js +125 -0
- package/engine/rules/best-practice/accesskeys.js +29 -0
- package/engine/rules/best-practice/aria-label-misuse.js +22 -0
- package/engine/rules/best-practice/button-type.js +15 -0
- package/engine/rules/best-practice/empty-heading.js +19 -0
- package/engine/rules/best-practice/fieldset-legend.js +35 -0
- package/engine/rules/best-practice/heading-order.js +30 -0
- package/engine/rules/best-practice/landmark-one-main.js +22 -0
- package/engine/rules/best-practice/landmark-top-level.js +46 -0
- package/engine/rules/best-practice/landmark-unique.js +84 -0
- package/engine/rules/best-practice/multiple-labels.js +23 -0
- package/engine/rules/best-practice/new-window-link.js +21 -0
- package/engine/rules/best-practice/no-autofocus.js +16 -0
- package/engine/rules/best-practice/page-heading-one.js +17 -0
- package/engine/rules/best-practice/positive-tabindex.js +17 -0
- package/engine/rules/best-practice/redundant-alt-phrase.js +22 -0
- package/engine/rules/best-practice/redundant-aria-label.js +18 -0
- package/engine/rules/best-practice/redundant-aria.js +32 -0
- package/engine/rules/best-practice/redundant-image-alt.js +20 -0
- package/engine/rules/best-practice/redundant-role.js +62 -0
- package/engine/rules/best-practice/redundant-tabindex.js +15 -0
- package/engine/rules/best-practice/region.js +73 -0
- package/engine/rules/index.js +179 -0
- package/engine/rules/wcag/1.1.1-area-alt.js +18 -0
- package/engine/rules/wcag/1.1.1-image-alt.js +48 -0
- package/engine/rules/wcag/1.1.1-input-image-alt.js +17 -0
- package/engine/rules/wcag/1.1.1-object-alt.js +17 -0
- package/engine/rules/wcag/1.1.1-svg-img-alt.js +34 -0
- package/engine/rules/wcag/1.2.2-media-captions.js +19 -0
- package/engine/rules/wcag/1.3.1-aria-required-children.js +66 -0
- package/engine/rules/wcag/1.3.1-aria-required-parent.js +49 -0
- package/engine/rules/wcag/1.3.1-definition-list.js +21 -0
- package/engine/rules/wcag/1.3.1-dlitem-parent.js +17 -0
- package/engine/rules/wcag/1.3.1-form-label.js +81 -0
- package/engine/rules/wcag/1.3.1-list-structure.js +59 -0
- package/engine/rules/wcag/1.3.1-listitem-parent.js +33 -0
- package/engine/rules/wcag/1.3.1-p-as-heading.js +25 -0
- package/engine/rules/wcag/1.3.1-table-headers.js +61 -0
- package/engine/rules/wcag/1.3.5-autocomplete-valid.js +38 -0
- package/engine/rules/wcag/1.4.1-link-in-text-block.js +74 -0
- package/engine/rules/wcag/1.4.10-reflow.js +25 -0
- package/engine/rules/wcag/1.4.11-non-text-contrast.js +109 -0
- package/engine/rules/wcag/1.4.12-text-spacing.js +71 -0
- package/engine/rules/wcag/1.4.2-audio-control.js +40 -0
- package/engine/rules/wcag/1.4.3-color-contrast.js +462 -0
- package/engine/rules/wcag/1.4.3-control-contrast.js +72 -0
- package/engine/rules/wcag/1.4.4-meta-viewport.js +22 -0
- package/engine/rules/wcag/1.4.6-contrast-enhanced.js +11 -0
- package/engine/rules/wcag/2.1.1-scrollable-region-focusable.js +48 -0
- package/engine/rules/wcag/2.2.1-meta-refresh.js +20 -0
- package/engine/rules/wcag/2.2.2-pause-stop-hide.js +25 -0
- package/engine/rules/wcag/2.4.1-bypass-blocks.js +36 -0
- package/engine/rules/wcag/2.4.11-focus-not-obscured.js +80 -0
- package/engine/rules/wcag/2.4.2-document-title.js +18 -0
- package/engine/rules/wcag/2.4.4-link-name.js +19 -0
- package/engine/rules/wcag/2.4.7-focus-visible.js +46 -0
- package/engine/rules/wcag/2.5.3-label-in-name.js +88 -0
- package/engine/rules/wcag/2.5.5-target-size-enhanced.js +14 -0
- package/engine/rules/wcag/2.5.8-target-size.js +159 -0
- package/engine/rules/wcag/3.1.1-html-lang.js +31 -0
- package/engine/rules/wcag/3.1.2-valid-lang-parts.js +20 -0
- package/engine/rules/wcag/3.3.8-auth-field-obstruction.js +37 -0
- package/engine/rules/wcag/4.1.2-aria-allowed-attr.js +44 -0
- package/engine/rules/wcag/4.1.2-aria-attr-valid.js +50 -0
- package/engine/rules/wcag/4.1.2-aria-field-name.js +54 -0
- package/engine/rules/wcag/4.1.2-aria-hidden-focus.js +48 -0
- package/engine/rules/wcag/4.1.2-aria-valid-refs.js +61 -0
- package/engine/rules/wcag/4.1.2-button-name.js +17 -0
- package/engine/rules/wcag/4.1.2-dialog-name.js +35 -0
- package/engine/rules/wcag/4.1.2-frame-title.js +17 -0
- package/engine/rules/wcag/4.1.2-label-for-valid.js +25 -0
- package/engine/rules/wcag/4.1.2-nested-interactive.js +30 -0
- package/engine/rules/wcag/4.1.2-role-required-aria.js +36 -0
- package/engine/rules/wcag/4.1.2-valid-role.js +52 -0
- package/engine/wcag22.js +116 -0
- package/engine/wcag30-draft.js +149 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Yarham
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# pour engine
|
|
2
|
+
|
|
3
|
+
The accessibility audit engine behind [pour.dev](https://pour.dev) — a
|
|
4
|
+
clean-room WCAG engine written from the W3C specifications. Zero
|
|
5
|
+
dependencies, runs entirely in the browser, returns structured per-rule
|
|
6
|
+
results with the exact failing elements and the evidence for each verdict.
|
|
7
|
+
|
|
8
|
+
## Rules & coverage
|
|
9
|
+
|
|
10
|
+
**WCAG only, by design.** The engine tracks the WCAG spec exclusively — no
|
|
11
|
+
Section 508, EN 301 549, or other standards. The full WCAG 2.2 catalog (all
|
|
12
|
+
86 success criteria) lives in `engine/wcag22.js`, and
|
|
13
|
+
[`engine/COVERAGE.md`](engine/COVERAGE.md) shows exactly which criteria are
|
|
14
|
+
automated, partially automated, or need human review. Rules pierce open
|
|
15
|
+
shadow DOM, callers can cover same-origin frames by running the engine per
|
|
16
|
+
document, and every result carries its WCAG criterion, severity, a CSS path
|
|
17
|
+
to the element, and an HTML snippet.
|
|
18
|
+
|
|
19
|
+
Automation has limits, and the engine is honest about them: criteria no tool
|
|
20
|
+
can verify are returned as a `manualReview` checklist instead of being
|
|
21
|
+
silently skipped, and findings the engine can't judge conclusively land in
|
|
22
|
+
`incomplete` rather than being guessed at.
|
|
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
|
+
## Getting started
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install pour-engine
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> Not yet published to npm — `package.json` is marked `"private": true` while
|
|
35
|
+
> this repository stays private. Remove that flag when open-sourcing.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { run, name, version } from 'pour-engine';
|
|
39
|
+
|
|
40
|
+
// Audit the current document (browser, extension content script, bookmarklet…)
|
|
41
|
+
const results = await run(document, {
|
|
42
|
+
// optional: filter rules by tag. Tags are cumulative — WCAG 2.2 AA means
|
|
43
|
+
// every A/AA rule from 2.0 through 2.2. Omit to run everything.
|
|
44
|
+
tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'],
|
|
45
|
+
// standard: 'wcag30-draft', // optional: experimental WCAG 3.0 draft mode
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(results.violations, results.passes, results.incomplete);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`run(context, options, onProgress)` accepts any Document (the top document
|
|
52
|
+
or an iframe's), an options object, and an optional progress callback
|
|
53
|
+
invoked before and after each rule — useful for live progress UI. Results
|
|
54
|
+
include `violations`, `passes`, `incomplete`, `inapplicable`,
|
|
55
|
+
`manualReview`, and per-rule timings.
|
|
56
|
+
|
|
57
|
+
## Philosophy
|
|
58
|
+
|
|
59
|
+
- **Spec-first.** Every rule and helper was written from the W3C
|
|
60
|
+
specifications (WCAG, ARIA, accname), not from other tools' behavior.
|
|
61
|
+
When a rule tightens, it tightens toward the spec.
|
|
62
|
+
- **Zero dependencies.** The engine ships nothing but its own code, so it
|
|
63
|
+
can run anywhere a DOM exists — content scripts, bookmarklets, test
|
|
64
|
+
harnesses — without bundling baggage or supply-chain surface.
|
|
65
|
+
- **No false authority.** A result is a violation only when the spec says
|
|
66
|
+
so. Everything uncertain is `incomplete` or `manualReview`, never a guess
|
|
67
|
+
dressed up as a finding.
|
|
68
|
+
|
|
69
|
+
## Supported environments
|
|
70
|
+
|
|
71
|
+
Any evergreen browser (the pour extension ships to Chromium; the bookmarklet
|
|
72
|
+
runs wherever it's dropped). The engine needs a live DOM — it does not parse
|
|
73
|
+
static HTML strings. For Node-based testing, drive it through a real browser
|
|
74
|
+
(Puppeteer/Playwright).
|
|
75
|
+
|
|
76
|
+
## Package contents
|
|
77
|
+
|
|
78
|
+
- `engine/index.js` — the runner: rule selection, scoping, timings, results.
|
|
79
|
+
- `engine/rules/` — one file per rule, named by criterion
|
|
80
|
+
(`1.1.1-image-alt.js`), plus the catalog index.
|
|
81
|
+
- `engine/lib/` — shared helpers: DOM traversal, accessible-name
|
|
82
|
+
computation, contrast math, role model.
|
|
83
|
+
- `engine/wcag22.js`, `engine/wcag30-draft.js` — the WCAG catalogs.
|
|
84
|
+
- `config/project.config.js` — package-local stand-in for the pour
|
|
85
|
+
monorepo's project config (engine name + version only).
|
|
86
|
+
|
|
87
|
+
The `engine/` directory is a byte-identical copy of `src/engine/` in the
|
|
88
|
+
pour monorepo, which is the current source of truth — make changes there and
|
|
89
|
+
sync them here. The test suite (annotated testbed pages and the verification
|
|
90
|
+
runner) also lives in the monorepo and is not part of this package yet.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
Every rule and helper was written from the W3C specifications (WCAG 2.2,
|
|
95
|
+
WAI-ARIA, accname). Rule ids and tag names follow the vocabulary the
|
|
96
|
+
accessibility-tooling ecosystem already shares, so reports interoperate
|
|
97
|
+
with existing pipelines.
|
|
98
|
+
|
|
99
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Package-local stand-in for the pour monorepo's project config. The engine
|
|
2
|
+
// source under engine/ is a byte-identical copy of the monorepo's src/engine/
|
|
3
|
+
// — it imports '../config/project.config.js', and this file satisfies that
|
|
4
|
+
// import so the copy needs no edits. Keep engine.version in step with
|
|
5
|
+
// package.json's version.
|
|
6
|
+
export default {
|
|
7
|
+
engine: {
|
|
8
|
+
name: 'pour engine',
|
|
9
|
+
version: '1.1.0',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# WCAG 2.2 coverage
|
|
2
|
+
|
|
3
|
+
Generated by `node scripts/wcag-coverage.js` — do not edit by hand.
|
|
4
|
+
|
|
5
|
+
All 86 success criteria of WCAG 2.2, and how the native engine covers them.
|
|
6
|
+
"Partially automated" means our rules catch common failures but cannot prove conformance;
|
|
7
|
+
"Manual review" criteria need a human (most of the spec — true for every automated tool).
|
|
8
|
+
|
|
9
|
+
## Perceivable
|
|
10
|
+
|
|
11
|
+
| SC | Name | Level | Coverage | Rules |
|
|
12
|
+
|---|---|---|---|---|
|
|
13
|
+
| 1.1.1 | [Non-text Content](https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html) | A | Partially automated | area-alt, image-alt, input-image-alt, object-alt, svg-img-alt |
|
|
14
|
+
| 1.2.1 | [Audio-only and Video-only (Prerecorded)](https://www.w3.org/WAI/WCAG22/Understanding/audio-only-and-video-only-prerecorded.html) | A | Manual review | — |
|
|
15
|
+
| 1.2.2 | [Captions (Prerecorded)](https://www.w3.org/WAI/WCAG22/Understanding/captions-prerecorded.html) | A | Partially automated | media-captions |
|
|
16
|
+
| 1.2.3 | [Audio Description or Media Alternative (Prerecorded)](https://www.w3.org/WAI/WCAG22/Understanding/audio-description-or-media-alternative-prerecorded.html) | A | Manual review | — |
|
|
17
|
+
| 1.2.4 | [Captions (Live)](https://www.w3.org/WAI/WCAG22/Understanding/captions-live.html) | AA | Manual review | — |
|
|
18
|
+
| 1.2.5 | [Audio Description (Prerecorded)](https://www.w3.org/WAI/WCAG22/Understanding/audio-description-prerecorded.html) | AA | Manual review | — |
|
|
19
|
+
| 1.2.6 | [Sign Language (Prerecorded)](https://www.w3.org/WAI/WCAG22/Understanding/sign-language-prerecorded.html) | AAA | Manual review | — |
|
|
20
|
+
| 1.2.7 | [Extended Audio Description (Prerecorded)](https://www.w3.org/WAI/WCAG22/Understanding/extended-audio-description-prerecorded.html) | AAA | Manual review | — |
|
|
21
|
+
| 1.2.8 | [Media Alternative (Prerecorded)](https://www.w3.org/WAI/WCAG22/Understanding/media-alternative-prerecorded.html) | AAA | Manual review | — |
|
|
22
|
+
| 1.2.9 | [Audio-only (Live)](https://www.w3.org/WAI/WCAG22/Understanding/audio-only-live.html) | AAA | Manual review | — |
|
|
23
|
+
| 1.3.1 | [Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) | A | Partially automated | aria-required-children, aria-required-parent, definition-list, dlitem-parent, form-label, list-structure, listitem-parent, p-as-heading, table-headers |
|
|
24
|
+
| 1.3.2 | [Meaningful Sequence](https://www.w3.org/WAI/WCAG22/Understanding/meaningful-sequence.html) | A | Manual review | — |
|
|
25
|
+
| 1.3.3 | [Sensory Characteristics](https://www.w3.org/WAI/WCAG22/Understanding/sensory-characteristics.html) | A | Manual review | — |
|
|
26
|
+
| 1.3.4 | [Orientation](https://www.w3.org/WAI/WCAG22/Understanding/orientation.html) | AA | Manual review | — |
|
|
27
|
+
| 1.3.5 | [Identify Input Purpose](https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html) | AA | Partially automated | autocomplete-valid |
|
|
28
|
+
| 1.3.6 | [Identify Purpose](https://www.w3.org/WAI/WCAG22/Understanding/identify-purpose.html) | AAA | Manual review | — |
|
|
29
|
+
| 1.4.1 | [Use of Color](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html) | A | Partially automated | link-in-text-block |
|
|
30
|
+
| 1.4.2 | [Audio Control](https://www.w3.org/WAI/WCAG22/Understanding/audio-control.html) | A | Partially automated | audio-control |
|
|
31
|
+
| 1.4.3 | [Contrast (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html) | AA | Partially automated | color-contrast, control-contrast |
|
|
32
|
+
| 1.4.4 | [Resize Text](https://www.w3.org/WAI/WCAG22/Understanding/resize-text.html) | AA | Partially automated | meta-viewport |
|
|
33
|
+
| 1.4.5 | [Images of Text](https://www.w3.org/WAI/WCAG22/Understanding/images-of-text.html) | AA | Manual review | — |
|
|
34
|
+
| 1.4.6 | [Contrast (Enhanced)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-enhanced.html) | AAA | Partially automated | color-contrast-enhanced |
|
|
35
|
+
| 1.4.7 | [Low or No Background Audio](https://www.w3.org/WAI/WCAG22/Understanding/low-or-no-background-audio.html) | AAA | Manual review | — |
|
|
36
|
+
| 1.4.8 | [Visual Presentation](https://www.w3.org/WAI/WCAG22/Understanding/visual-presentation.html) | AAA | Manual review | — |
|
|
37
|
+
| 1.4.9 | [Images of Text (No Exception)](https://www.w3.org/WAI/WCAG22/Understanding/images-of-text-no-exception.html) | AAA | Manual review | — |
|
|
38
|
+
| 1.4.10 | [Reflow](https://www.w3.org/WAI/WCAG22/Understanding/reflow.html) | AA | Partially automated | reflow |
|
|
39
|
+
| 1.4.11 | [Non-text Contrast](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html) | AA | Partially automated | non-text-contrast |
|
|
40
|
+
| 1.4.12 | [Text Spacing](https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html) | AA | Partially automated | text-spacing |
|
|
41
|
+
| 1.4.13 | [Content on Hover or Focus](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html) | AA | Manual review | — |
|
|
42
|
+
|
|
43
|
+
## Operable
|
|
44
|
+
|
|
45
|
+
| SC | Name | Level | Coverage | Rules |
|
|
46
|
+
|---|---|---|---|---|
|
|
47
|
+
| 2.1.1 | [Keyboard](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) | A | Partially automated | scrollable-region-focusable |
|
|
48
|
+
| 2.1.2 | [No Keyboard Trap](https://www.w3.org/WAI/WCAG22/Understanding/no-keyboard-trap.html) | A | Manual review | — |
|
|
49
|
+
| 2.1.3 | [Keyboard (No Exception)](https://www.w3.org/WAI/WCAG22/Understanding/keyboard-no-exception.html) | AAA | Partially automated | scrollable-region-focusable |
|
|
50
|
+
| 2.1.4 | [Character Key Shortcuts](https://www.w3.org/WAI/WCAG22/Understanding/character-key-shortcuts.html) | A | Manual review | — |
|
|
51
|
+
| 2.2.1 | [Timing Adjustable](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html) | A | Partially automated | meta-refresh |
|
|
52
|
+
| 2.2.2 | [Pause, Stop, Hide](https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html) | A | Partially automated | pause-stop-hide |
|
|
53
|
+
| 2.2.3 | [No Timing](https://www.w3.org/WAI/WCAG22/Understanding/no-timing.html) | AAA | Manual review | — |
|
|
54
|
+
| 2.2.4 | [Interruptions](https://www.w3.org/WAI/WCAG22/Understanding/interruptions.html) | AAA | Manual review | — |
|
|
55
|
+
| 2.2.5 | [Re-authenticating](https://www.w3.org/WAI/WCAG22/Understanding/re-authenticating.html) | AAA | Manual review | — |
|
|
56
|
+
| 2.2.6 | [Timeouts](https://www.w3.org/WAI/WCAG22/Understanding/timeouts.html) | AAA | Manual review | — |
|
|
57
|
+
| 2.3.1 | [Three Flashes or Below Threshold](https://www.w3.org/WAI/WCAG22/Understanding/three-flashes-or-below-threshold.html) | A | Manual review | — |
|
|
58
|
+
| 2.3.2 | [Three Flashes](https://www.w3.org/WAI/WCAG22/Understanding/three-flashes.html) | AAA | Manual review | — |
|
|
59
|
+
| 2.3.3 | [Animation from Interactions](https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html) | AAA | Manual review | — |
|
|
60
|
+
| 2.4.1 | [Bypass Blocks](https://www.w3.org/WAI/WCAG22/Understanding/bypass-blocks.html) | A | Partially automated | bypass-blocks |
|
|
61
|
+
| 2.4.2 | [Page Titled](https://www.w3.org/WAI/WCAG22/Understanding/page-titled.html) | A | Automated | document-title |
|
|
62
|
+
| 2.4.3 | [Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) | A | Manual review | — |
|
|
63
|
+
| 2.4.4 | [Link Purpose (In Context)](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html) | A | Partially automated | area-alt, link-name |
|
|
64
|
+
| 2.4.5 | [Multiple Ways](https://www.w3.org/WAI/WCAG22/Understanding/multiple-ways.html) | AA | Manual review | — |
|
|
65
|
+
| 2.4.6 | [Headings and Labels](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html) | AA | Manual review | — |
|
|
66
|
+
| 2.4.7 | [Focus Visible](https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html) | AA | Partially automated | focus-visible |
|
|
67
|
+
| 2.4.8 | [Location](https://www.w3.org/WAI/WCAG22/Understanding/location.html) | AAA | Manual review | — |
|
|
68
|
+
| 2.4.9 | [Link Purpose (Link Only)](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-link-only.html) | AAA | Manual review | — |
|
|
69
|
+
| 2.4.10 | [Section Headings](https://www.w3.org/WAI/WCAG22/Understanding/section-headings.html) | AAA | Manual review | — |
|
|
70
|
+
| 2.4.11 | [Focus Not Obscured (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html) | AA | Partially automated | focus-not-obscured |
|
|
71
|
+
| 2.4.12 | [Focus Not Obscured (Enhanced)](https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-enhanced.html) | AAA | Manual review | — |
|
|
72
|
+
| 2.4.13 | [Focus Appearance](https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html) | AAA | Manual review | — |
|
|
73
|
+
| 2.5.1 | [Pointer Gestures](https://www.w3.org/WAI/WCAG22/Understanding/pointer-gestures.html) | A | Manual review | — |
|
|
74
|
+
| 2.5.2 | [Pointer Cancellation](https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation.html) | A | Manual review | — |
|
|
75
|
+
| 2.5.3 | [Label in Name](https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html) | A | Manual review | — |
|
|
76
|
+
| 2.5.4 | [Motion Actuation](https://www.w3.org/WAI/WCAG22/Understanding/motion-actuation.html) | A | Manual review | — |
|
|
77
|
+
| 2.5.5 | [Target Size (Enhanced)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html) | AAA | Partially automated | target-size-enhanced |
|
|
78
|
+
| 2.5.6 | [Concurrent Input Mechanisms](https://www.w3.org/WAI/WCAG22/Understanding/concurrent-input-mechanisms.html) | AAA | Manual review | — |
|
|
79
|
+
| 2.5.7 | [Dragging Movements](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) | AA | Manual review | — |
|
|
80
|
+
| 2.5.8 | [Target Size (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) | AA | Partially automated | target-size |
|
|
81
|
+
|
|
82
|
+
## Understandable
|
|
83
|
+
|
|
84
|
+
| SC | Name | Level | Coverage | Rules |
|
|
85
|
+
|---|---|---|---|---|
|
|
86
|
+
| 3.1.1 | [Language of Page](https://www.w3.org/WAI/WCAG22/Understanding/language-of-page.html) | A | Automated | html-lang |
|
|
87
|
+
| 3.1.2 | [Language of Parts](https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html) | AA | Partially automated | valid-lang-parts |
|
|
88
|
+
| 3.1.3 | [Unusual Words](https://www.w3.org/WAI/WCAG22/Understanding/unusual-words.html) | AAA | Manual review | — |
|
|
89
|
+
| 3.1.4 | [Abbreviations](https://www.w3.org/WAI/WCAG22/Understanding/abbreviations.html) | AAA | Manual review | — |
|
|
90
|
+
| 3.1.5 | [Reading Level](https://www.w3.org/WAI/WCAG22/Understanding/reading-level.html) | AAA | Manual review | — |
|
|
91
|
+
| 3.1.6 | [Pronunciation](https://www.w3.org/WAI/WCAG22/Understanding/pronunciation.html) | AAA | Manual review | — |
|
|
92
|
+
| 3.2.1 | [On Focus](https://www.w3.org/WAI/WCAG22/Understanding/on-focus.html) | A | Manual review | — |
|
|
93
|
+
| 3.2.2 | [On Input](https://www.w3.org/WAI/WCAG22/Understanding/on-input.html) | A | Manual review | — |
|
|
94
|
+
| 3.2.3 | [Consistent Navigation](https://www.w3.org/WAI/WCAG22/Understanding/consistent-navigation.html) | AA | Manual review | — |
|
|
95
|
+
| 3.2.4 | [Consistent Identification](https://www.w3.org/WAI/WCAG22/Understanding/consistent-identification.html) | AA | Manual review | — |
|
|
96
|
+
| 3.2.5 | [Change on Request](https://www.w3.org/WAI/WCAG22/Understanding/change-on-request.html) | AAA | Manual review | — |
|
|
97
|
+
| 3.2.6 | [Consistent Help](https://www.w3.org/WAI/WCAG22/Understanding/consistent-help.html) | A | Manual review | — |
|
|
98
|
+
| 3.3.1 | [Error Identification](https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html) | A | Manual review | — |
|
|
99
|
+
| 3.3.2 | [Labels or Instructions](https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html) | A | Partially automated | form-label |
|
|
100
|
+
| 3.3.3 | [Error Suggestion](https://www.w3.org/WAI/WCAG22/Understanding/error-suggestion.html) | AA | Manual review | — |
|
|
101
|
+
| 3.3.4 | [Error Prevention (Legal, Financial, Data)](https://www.w3.org/WAI/WCAG22/Understanding/error-prevention-legal-financial-data.html) | AA | Manual review | — |
|
|
102
|
+
| 3.3.5 | [Help](https://www.w3.org/WAI/WCAG22/Understanding/help.html) | AAA | Manual review | — |
|
|
103
|
+
| 3.3.6 | [Error Prevention (All)](https://www.w3.org/WAI/WCAG22/Understanding/error-prevention-all.html) | AAA | Manual review | — |
|
|
104
|
+
| 3.3.7 | [Redundant Entry](https://www.w3.org/WAI/WCAG22/Understanding/redundant-entry.html) | A | Manual review | — |
|
|
105
|
+
| 3.3.8 | [Accessible Authentication (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html) | AA | Partially automated | auth-field-obstruction |
|
|
106
|
+
| 3.3.9 | [Accessible Authentication (Enhanced)](https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-enhanced.html) | AAA | Manual review | — |
|
|
107
|
+
|
|
108
|
+
## Robust
|
|
109
|
+
|
|
110
|
+
| SC | Name | Level | Coverage | Rules |
|
|
111
|
+
|---|---|---|---|---|
|
|
112
|
+
| 4.1.2 | [Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) | A | Partially automated | input-image-alt, form-label, link-name, aria-allowed-attr, aria-attr-valid, aria-field-name, aria-hidden-focus, aria-valid-refs, button-name, dialog-name, frame-title, label-for-valid, nested-interactive, role-required-aria, valid-role |
|
|
113
|
+
| 4.1.3 | [Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html) | AA | Manual review | — |
|
|
114
|
+
|
|
115
|
+
**Totals:** 2 automated · 26 partially automated · 58 manual — with 71 rules.
|
package/engine/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# pour engine
|
|
2
|
+
|
|
3
|
+
Our own accessibility engine (name/version defined in
|
|
4
|
+
`src/config/project.config.js`) — written from the W3C specifications, and
|
|
5
|
+
the product's only engine.
|
|
6
|
+
|
|
7
|
+
**WCAG only, by design.** pour engine tracks the WCAG spec exclusively — no
|
|
8
|
+
Section 508, EN 301 549, or other standards. The full WCAG 2.2 catalog (all
|
|
9
|
+
86 success criteria) lives in `wcag22.js`, and `COVERAGE.md` (generated by
|
|
10
|
+
`node scripts/wcag-coverage.js`) shows exactly which criteria are automated,
|
|
11
|
+
partially automated, or need human review.
|
|
12
|
+
|
|
13
|
+
## Provenance
|
|
14
|
+
|
|
15
|
+
Every rule and helper is written from the W3C specifications — WCAG 2.2,
|
|
16
|
+
WAI-ARIA, and the accessible-name computation spec. Where a threshold
|
|
17
|
+
appears in the code (4.5:1, 24px, 72000 s), it comes from the spec text
|
|
18
|
+
itself.
|
|
19
|
+
|
|
20
|
+
The result vocabulary follows the conventions the accessibility-tooling
|
|
21
|
+
ecosystem already speaks, so reports interoperate with existing pipelines:
|
|
22
|
+
rule ids like `image-alt`, the `wcag2a`/`wcag111` tag scheme, the
|
|
23
|
+
`critical/serious/moderate/minor` impact scale, and the result schema
|
|
24
|
+
(`violations/passes/incomplete/inapplicable`, `helpUrl`, `failureSummary`).
|
|
25
|
+
All user-facing prose (`help`, `message`, `fix`) is original.
|
|
26
|
+
|
|
27
|
+
## What it aims to do well
|
|
28
|
+
|
|
29
|
+
1. **Actionable messages.** Every finding says *how to fix it*, with
|
|
30
|
+
concrete suggested code for the specific element (see any rule's
|
|
31
|
+
`fix()`), not just what failed in spec language.
|
|
32
|
+
2. **Readable source.** Each rule is one small file a contributor can read
|
|
33
|
+
in a minute — no rule → checks → metadata indirection.
|
|
34
|
+
3. **Resolved verdicts over "incomplete" noise.** The engine works to
|
|
35
|
+
resolve backgrounds, backdrops, and paint order before giving up — and
|
|
36
|
+
when it genuinely can't judge, it says so plainly instead of guessing.
|
|
37
|
+
4. **Size.** The engine is a few KB, so the extension and bookmarklet stay
|
|
38
|
+
light.
|
|
39
|
+
5. **Filters integration.** Vision/sensory filters and the engine share
|
|
40
|
+
colour math and DOM helpers.
|
|
41
|
+
6. **"Overdoing it" rules.** Best-practice rules that flag *redundant*
|
|
42
|
+
accessibility — role="list" on a `<ul>`, aria-required duplicating
|
|
43
|
+
required, aria-label repeating visible text, alt text starting with
|
|
44
|
+
"image of", tabindex="0" on a `<button>`. Leaner markup is better
|
|
45
|
+
markup. (Enable "Best practices" in settings to see them.)
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
src/engine/
|
|
51
|
+
index.js run(context, {tags}, onProgress) → results
|
|
52
|
+
wcag22.js the full WCAG 2.2 success-criteria catalog
|
|
53
|
+
lib/
|
|
54
|
+
dom.js visibility, CSS-path selectors, html snippets
|
|
55
|
+
accessible-name.js accessible-name computation
|
|
56
|
+
contrast.js colour parsing, luminance, contrast + pixel sampling
|
|
57
|
+
roles.js ARIA role model (implicit roles, allowed attributes)
|
|
58
|
+
rules/
|
|
59
|
+
index.js rule registry
|
|
60
|
+
wcag/<rule>.js spec-required rules (tagged with wcag* criteria)
|
|
61
|
+
best-practice/<rule>.js opt-in extras (tagged 'best-practice')
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A rule is a small object:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
export default {
|
|
68
|
+
id: 'image-alt',
|
|
69
|
+
impact: 'critical', // critical | serious | moderate | minor
|
|
70
|
+
tags: ['wcag2a', 'wcag111'], // version/level tags + criterion tags
|
|
71
|
+
help: 'Every <img> needs a text alternative',
|
|
72
|
+
helpUrl: 'https://…',
|
|
73
|
+
selector: 'img', // which elements the rule applies to
|
|
74
|
+
visibleOnly: true, // skip hidden elements (default true)
|
|
75
|
+
evaluate(element, helpers) {
|
|
76
|
+
// → { status: 'pass' } or { status: 'fail'|'incomplete', message, fix }
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`run(context, { tags })` selects rules by tag, evaluates each matching
|
|
82
|
+
element, and buckets results into `violations / passes / incomplete /
|
|
83
|
+
inapplicable`, each node carrying its target selector, html snippet, and a
|
|
84
|
+
plain-language failure summary with a concrete fix.
|
|
85
|
+
|
|
86
|
+
Rules that need to see the whole page at once (e.g. heading order) implement
|
|
87
|
+
`evaluateAll(elements, helpers)` instead of `evaluate`.
|
|
88
|
+
|
|
89
|
+
## The spec is the authority
|
|
90
|
+
|
|
91
|
+
Every rule is adjudicated against the WCAG, accname, and ARIA specs — those
|
|
92
|
+
documents decide what is a violation. The engine is also validated
|
|
93
|
+
continuously against pixel-measured ground truth on real sites (see the
|
|
94
|
+
repository's engine lab) so its verdicts answer to rendered reality, not
|
|
95
|
+
just to static analysis.
|
|
96
|
+
|
|
97
|
+
## Full-spec coverage model
|
|
98
|
+
|
|
99
|
+
Rules alone can never "cover WCAG" — most criteria need human judgment.
|
|
100
|
+
pour engine therefore reports on **every** in-scope criterion, one of two ways:
|
|
101
|
+
|
|
102
|
+
1. **Automated/partial** — the rules in `rules/`.
|
|
103
|
+
2. **Manual review** — every audit's results include `manualReview`: the
|
|
104
|
+
in-scope criteria (filtered by the requested WCAG version and level)
|
|
105
|
+
that automation can't judge, rendered as a checklist in the extension.
|
|
106
|
+
|
|
107
|
+
Nothing in the spec is silently skipped — that claim is the product.
|
|
108
|
+
|
|
109
|
+
## Honest current state
|
|
110
|
+
|
|
111
|
+
61 rules, hardened on real sites and the `testbeds/` pages. Known gaps:
|
|
112
|
+
shadow DOM and iframe traversal, the full accname spec (ours is a close
|
|
113
|
+
approximation), and the language subtag registry (BCP 47 shape check only).
|
|
114
|
+
Grow it rule by rule; the test beds' `expect:` annotations are the
|
|
115
|
+
regression suite.
|
package/engine/index.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// DY Engine runner: evaluates every applicable rule against the page and
|
|
2
|
+
// returns structured results (violations / passes / incomplete /
|
|
3
|
+
// inapplicable, plus the manual-review criteria and per-rule timings).
|
|
4
|
+
import config from '../config/project.config.js';
|
|
5
|
+
import rules from './rules/index.js';
|
|
6
|
+
import wcagCatalog from './wcag22.js';
|
|
7
|
+
import wcag30Draft from './wcag30-draft.js';
|
|
8
|
+
import { isVisible, isRendered, cssPath, htmlSnippet, ownText, collectRoots } from './lib/dom.js';
|
|
9
|
+
import { accessibleName } from './lib/accessible-name.js';
|
|
10
|
+
import { resetAuditCaches } from './lib/contrast.js';
|
|
11
|
+
|
|
12
|
+
export const name = config.engine.name;
|
|
13
|
+
export const version = config.engine.version;
|
|
14
|
+
|
|
15
|
+
const ruleHelpers = { isVisible, isRendered, cssPath, htmlSnippet, ownText, accessibleName };
|
|
16
|
+
|
|
17
|
+
/** Does this rule match the requested tag selection? Empty/absent = all rules. */
|
|
18
|
+
function ruleMatchesTags(rule, tags) {
|
|
19
|
+
if (!tags?.length) return true;
|
|
20
|
+
return rule.tags.some((tag) => tags.includes(tag));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Which WCAG version/level was requested, judged from the selected tags. */
|
|
24
|
+
function scopeFromTags(tags) {
|
|
25
|
+
if (!tags?.length) return { version: '2.2', level: 'AAA' }; // no filter → everything
|
|
26
|
+
const version = tags.some((t) => t.startsWith('wcag22')) ? '2.2'
|
|
27
|
+
: tags.some((t) => t.startsWith('wcag21')) ? '2.1' : '2.0';
|
|
28
|
+
const level = tags.some((t) => /aaa$/.test(t)) ? 'AAA'
|
|
29
|
+
: tags.some((t) => /aa$/.test(t)) ? 'AA' : 'A';
|
|
30
|
+
return { version, level };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const VERSION_ORDER = { '2.0': 0, '2.1': 1, '2.2': 2 };
|
|
34
|
+
const LEVEL_ORDER = { A: 0, AA: 1, AAA: 2 };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The WCAG criteria in scope that automation cannot fully judge — reported
|
|
38
|
+
* with every audit so that "full spec coverage" is real: rules handle what
|
|
39
|
+
* they can, and the rest is an explicit human checklist, never silence.
|
|
40
|
+
*/
|
|
41
|
+
function manualReviewCriteria(tags) {
|
|
42
|
+
const scope = scopeFromTags(tags);
|
|
43
|
+
return wcagCatalog
|
|
44
|
+
.filter((criterion) => criterion.automation !== 'auto')
|
|
45
|
+
.filter((criterion) => VERSION_ORDER[criterion.since] <= VERSION_ORDER[scope.version])
|
|
46
|
+
.filter((criterion) => LEVEL_ORDER[criterion.level] <= LEVEL_ORDER[scope.level])
|
|
47
|
+
.map(({ num, name: criterionName, level, principle, url, automation }) =>
|
|
48
|
+
({ num, name: criterionName, level, principle, url, automation }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True when the element, or any flat-tree ancestor (crossing shadow
|
|
53
|
+
* boundaries host-ward), matches the exclude selector. Lets a testing tool
|
|
54
|
+
* keep its own injected UI out of the audit — a panel drawn over the page
|
|
55
|
+
* must never appear in the page's results.
|
|
56
|
+
*/
|
|
57
|
+
function isExcluded(element, selector) {
|
|
58
|
+
let node = element;
|
|
59
|
+
while (node) {
|
|
60
|
+
if (node.closest?.(selector)) return true;
|
|
61
|
+
node = node.getRootNode()?.host ?? null;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Serialize one evaluated element into a result node. */
|
|
67
|
+
function toResultNode(element, outcome) {
|
|
68
|
+
const node = {
|
|
69
|
+
target: [cssPath(element)],
|
|
70
|
+
html: htmlSnippet(element),
|
|
71
|
+
failureSummary: [outcome.message, outcome.fix && `Fix: ${outcome.fix}`].filter(Boolean).join('\n'),
|
|
72
|
+
};
|
|
73
|
+
// Structured extras from the rule (e.g. the exact colour pair behind a
|
|
74
|
+
// contrast failure) ride along for UIs that can use them.
|
|
75
|
+
if (outcome.data) node.data = outcome.data;
|
|
76
|
+
return node;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run the audit.
|
|
81
|
+
* @param {Document|Element} context - what to scan
|
|
82
|
+
* @param {{ tags?: string[], exclude?: string, standard?: string, signal?: AbortSignal }} options -
|
|
83
|
+
* rule selection by tag (wcag2a…wcag22aa, best-practice), empty selects
|
|
84
|
+
* every rule; exclude is a CSS selector — elements matching it, or inside
|
|
85
|
+
* a match (including across shadow boundaries), are left out of every
|
|
86
|
+
* rule; standard 'wcag30-draft' reframes the report against the WCAG 3.0
|
|
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)
|
|
92
|
+
* @param {(progress: object) => void} [onProgress] - called before each rule
|
|
93
|
+
* starts ({rule, running}), during a rule at each renderer yield with a
|
|
94
|
+
* fractional done (completed rules + this rule's evaluated-element share,
|
|
95
|
+
* so a bar fills smoothly through big element sets), and after it
|
|
96
|
+
* finishes ({rule, ms, counts})
|
|
97
|
+
*/
|
|
98
|
+
export async function run(context = document, options = {}, onProgress) {
|
|
99
|
+
const auditStarted = performance.now();
|
|
100
|
+
const draft30 = options.standard === 'wcag30-draft';
|
|
101
|
+
const results = {
|
|
102
|
+
testEngine: { name, version },
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
url: context.location?.href ?? context.ownerDocument?.location?.href ?? '',
|
|
105
|
+
standard: draft30 ? wcag30Draft.draftInfo : null,
|
|
106
|
+
violations: [],
|
|
107
|
+
passes: [],
|
|
108
|
+
incomplete: [],
|
|
109
|
+
inapplicable: [],
|
|
110
|
+
manualReview: draft30 ? wcag30Draft.manualReviewChecklist() : manualReviewCriteria(options.tags),
|
|
111
|
+
ruleTimings: [],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// `disabled: true` keeps a rule in the codebase (and coverage docs) but
|
|
115
|
+
// out of every run — for rules parked pending quality review.
|
|
116
|
+
const activeRules = rules.filter((rule) => !rule.disabled && ruleMatchesTags(rule, options.tags));
|
|
117
|
+
// Per-audit caches (resolved backgrounds, opacities) must start fresh —
|
|
118
|
+
// the page may have changed since the last run.
|
|
119
|
+
resetAuditCaches();
|
|
120
|
+
// Every queryable root — the page plus all open shadow trees — collected
|
|
121
|
+
// once so rules see web-component content without re-walking per rule.
|
|
122
|
+
const roots = collectRoots(context.querySelectorAll ? context : document);
|
|
123
|
+
// Universal-selector rules (aria attribute checks) share one full scan
|
|
124
|
+
// instead of each re-enumerating every element on the page.
|
|
125
|
+
let universalScan = null;
|
|
126
|
+
const elementsFor = (selector) => {
|
|
127
|
+
if (selector === '*') {
|
|
128
|
+
universalScan ??= roots.flatMap((root) => [...root.querySelectorAll('*')]);
|
|
129
|
+
return universalScan;
|
|
130
|
+
}
|
|
131
|
+
return roots.flatMap((root) => [...root.querySelectorAll(selector)]);
|
|
132
|
+
};
|
|
133
|
+
let completed = 0;
|
|
134
|
+
// Running totals so the UI can count issues up live as rules finish —
|
|
135
|
+
// both affected elements and distinct rules ("types") per severity.
|
|
136
|
+
const liveCounts = { critical: 0, serious: 0, moderate: 0, minor: 0, review: 0 };
|
|
137
|
+
const liveTypeCounts = { critical: 0, serious: 0, moderate: 0, minor: 0, review: 0 };
|
|
138
|
+
|
|
139
|
+
// The audit shares the page's main thread. Rules only await microtasks,
|
|
140
|
+
// so without a real yield the whole run is one long task and the browser
|
|
141
|
+
// never paints — in-page progress UI (the bookmarklet panel) would appear
|
|
142
|
+
// only when everything is done, and the audited page itself freezes.
|
|
143
|
+
// Yield about once a frame; between rules is the granularity we have.
|
|
144
|
+
let lastYield = performance.now();
|
|
145
|
+
// Yield via MessageChannel: a normal macrotask like setTimeout(0) — fair
|
|
146
|
+
// FIFO with the audited page's own tasks (deliberately NOT
|
|
147
|
+
// scheduler.yield(), whose continuations outprioritize and starve them) —
|
|
148
|
+
// but WITHOUT the ~4ms nested-timer clamp, which added phantom wall time
|
|
149
|
+
// across the hundreds of yields of a large audit.
|
|
150
|
+
const yieldChannel = typeof MessageChannel === 'function' ? new MessageChannel() : null;
|
|
151
|
+
const nextTask = () => new Promise((resolve) => {
|
|
152
|
+
if (!yieldChannel) { setTimeout(resolve, 0); return; }
|
|
153
|
+
yieldChannel.port1.onmessage = resolve; // one yield in flight at a time
|
|
154
|
+
yieldChannel.port2.postMessage(0);
|
|
155
|
+
});
|
|
156
|
+
const yieldToRenderer = async (beforeYield) => {
|
|
157
|
+
if (performance.now() - lastYield < 16) return;
|
|
158
|
+
// Runs only when a real yield (and so a paint) follows — the natural
|
|
159
|
+
// throttle for mid-rule progress reports.
|
|
160
|
+
beforeYield?.();
|
|
161
|
+
await nextTask();
|
|
162
|
+
lastYield = performance.now();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
for (const rule of activeRules) {
|
|
166
|
+
// Between rules is the abort granularity we have — the same boundary
|
|
167
|
+
// the renderer yield uses.
|
|
168
|
+
if (options.signal?.aborted) throw new DOMException('Audit stopped', 'AbortError');
|
|
169
|
+
await yieldToRenderer();
|
|
170
|
+
// Announce BEFORE running: the UI label must show the rule currently
|
|
171
|
+
// under test, not the one that just finished (a slow rule would
|
|
172
|
+
// otherwise sit blamed on its predecessor's name).
|
|
173
|
+
onProgress?.({ done: completed, total: activeRules.length, rule: rule.id, running: true });
|
|
174
|
+
const ruleStarted = performance.now();
|
|
175
|
+
|
|
176
|
+
let elements = elementsFor(rule.selector);
|
|
177
|
+
if (options.exclude) {
|
|
178
|
+
elements = elements.filter((element) => !isExcluded(element, options.exclude));
|
|
179
|
+
}
|
|
180
|
+
if (rule.visibleOnly !== false) {
|
|
181
|
+
// visibility: 'visual' → rendered on screen counts even inside
|
|
182
|
+
// aria-hidden (visual rules); default → must be exposed to AT too.
|
|
183
|
+
elements = elements.filter(rule.visibility === 'visual' ? isRendered : isVisible);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let outcomes;
|
|
187
|
+
if (rule.evaluateAll) {
|
|
188
|
+
outcomes = rule.evaluateAll(elements, ruleHelpers);
|
|
189
|
+
} else {
|
|
190
|
+
// Evaluate sequentially with periodic yields so ONE heavy rule on a
|
|
191
|
+
// huge element set can't freeze the page for its whole duration —
|
|
192
|
+
// and so a Stop lands mid-rule, not only between rules.
|
|
193
|
+
outcomes = new Array(elements.length);
|
|
194
|
+
for (let i = 0; i < elements.length; i++) {
|
|
195
|
+
if ((i & 31) === 0 && i > 0) {
|
|
196
|
+
if (options.signal?.aborted) throw new DOMException('Audit stopped', 'AbortError');
|
|
197
|
+
// Fractional done: this rule's slice of the bar fills as its
|
|
198
|
+
// elements are evaluated instead of jumping on completion.
|
|
199
|
+
await yieldToRenderer(() => onProgress?.({
|
|
200
|
+
done: completed + i / elements.length,
|
|
201
|
+
total: activeRules.length,
|
|
202
|
+
rule: rule.id,
|
|
203
|
+
running: true,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
outcomes[i] = await rule.evaluate(elements[i], ruleHelpers);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Only failures and review findings serialize element details — pass
|
|
211
|
+
// nodes are counted, not rendered: nothing consumes them, and building
|
|
212
|
+
// cssPath/snippets for every passing element dominated audit time on
|
|
213
|
+
// large pages.
|
|
214
|
+
const buckets = { fail: [], incomplete: [] };
|
|
215
|
+
let passCount = 0;
|
|
216
|
+
elements.forEach((element, i) => {
|
|
217
|
+
const status = outcomes[i].status;
|
|
218
|
+
if (status === 'pass') passCount += 1;
|
|
219
|
+
else buckets[status]?.push(toResultNode(element, outcomes[i]));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const ruleResult = {
|
|
223
|
+
id: rule.id,
|
|
224
|
+
impact: rule.impact,
|
|
225
|
+
tags: rule.tags,
|
|
226
|
+
help: rule.help,
|
|
227
|
+
description: rule.help,
|
|
228
|
+
helpUrl: rule.helpUrl,
|
|
229
|
+
};
|
|
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
|
+
if (!elements.length) results.inapplicable.push({ ...ruleResult, nodes: [] });
|
|
236
|
+
if (buckets.fail.length) results.violations.push({ ...ruleResult, nodes: buckets.fail });
|
|
237
|
+
if (buckets.incomplete.length) results.incomplete.push({ ...ruleResult, nodes: buckets.incomplete });
|
|
238
|
+
if (passCount) results.passes.push({ ...ruleResult, nodes: [], nodeCount: passCount });
|
|
239
|
+
|
|
240
|
+
liveCounts[rule.impact] = (liveCounts[rule.impact] ?? 0) + buckets.fail.length;
|
|
241
|
+
liveCounts.review += buckets.incomplete.length;
|
|
242
|
+
if (buckets.fail.length) liveTypeCounts[rule.impact] = (liveTypeCounts[rule.impact] ?? 0) + 1;
|
|
243
|
+
if (buckets.incomplete.length) liveTypeCounts.review += 1;
|
|
244
|
+
const ms = Math.round((performance.now() - ruleStarted) * 10) / 10;
|
|
245
|
+
results.ruleTimings.push({ rule: rule.id, ms, elements: elements.length });
|
|
246
|
+
onProgress?.({
|
|
247
|
+
done: ++completed,
|
|
248
|
+
total: activeRules.length,
|
|
249
|
+
rule: rule.id,
|
|
250
|
+
ms,
|
|
251
|
+
counts: { ...liveCounts },
|
|
252
|
+
typeCounts: { ...liveTypeCounts },
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
results.durationMs = Math.round(performance.now() - auditStarted);
|
|
257
|
+
return results;
|
|
258
|
+
}
|