@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/docs/EARL.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# EARL report
|
|
2
|
+
|
|
3
|
+
`@surea11y/core/earl` renders scan results as [EARL 1.0](https://www.w3.org/TR/EARL10-Schema/) in JSON-LD — the vocabulary the W3C publishes for stating "this tool tested this thing and got this result", and the format the [ACT Rules](https://act-rules.github.io/) community group accepts as an implementation report.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const { renderEarlReport } = require('@surea11y/core/earl');
|
|
7
|
+
|
|
8
|
+
const report = renderEarlReport(result, {
|
|
9
|
+
assertor: { name: 'surea11y', version: '1.7.0' },
|
|
10
|
+
mode: 'earl:automatic'
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
fs.writeFileSync('earl.jsonld', JSON.stringify(report, null, 2));
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it is for
|
|
17
|
+
|
|
18
|
+
Two audiences, and they want the same document for different reasons.
|
|
19
|
+
|
|
20
|
+
An **implementation report** tells the ACT Rules community group how this engine behaves against their test cases, which is what gets an engine listed alongside the other implementations. Listing is not endorsement and the W3C does not verify the data — the accurate phrasing is *"listed as an ACT implementation"*, never *"W3C certified"*. This engine's own report is published at <https://surea11y.github.io/act-report/act-report.jsonld> and regenerated by `scripts/act-report.js`.
|
|
21
|
+
|
|
22
|
+
A **consumer** gets an interchange format. EARL is what accessibility tooling reads when it has to combine results from more than one source — an automated scan and a manual audit, say, or several engines — because every assertion carries who asserted it and how. That is worth having whether or not anything is ever submitted anywhere.
|
|
23
|
+
|
|
24
|
+
## How it differs from the other reporters
|
|
25
|
+
|
|
26
|
+
[`SARIF.md`](./SARIF.md) and [`REPORT.md`](./REPORT.md) both carry **violations only**: they report `fail` and `cantTell` occurrences and drop everything else. EARL is the opposite. Every rule that ran becomes an assertion, `pass` and `inapplicable` included, because an implementation report is a claim about what the engine decided *everywhere*. A rule that stayed silent because it found nothing applicable is evidence, not noise — it is how a reader distinguishes "this engine checked and found nothing to check" from "this engine does not implement that rule at all".
|
|
27
|
+
|
|
28
|
+
## Shape
|
|
29
|
+
|
|
30
|
+
The graph groups by subject rather than being a flat list of assertions:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"@context": "https://www.w3.org/WAI/content-assets/wcag-act-rules/earl-context.json",
|
|
35
|
+
"@graph": [
|
|
36
|
+
{
|
|
37
|
+
"@type": "TestSubject",
|
|
38
|
+
"source": "https://example.test/",
|
|
39
|
+
"assertions": [
|
|
40
|
+
{
|
|
41
|
+
"@type": "Assertion",
|
|
42
|
+
"test": { "title": "img-alt-present", "isPartOf": ["WCAG2:non-text-content"] },
|
|
43
|
+
"result": { "outcome": "earl:failed" },
|
|
44
|
+
"assertedBy": {
|
|
45
|
+
"@type": "Assertor",
|
|
46
|
+
"name": "surea11y",
|
|
47
|
+
"release": { "@type": "Version", "revision": "1.7.0" }
|
|
48
|
+
},
|
|
49
|
+
"mode": "earl:automatic"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **`source`** is the scanned URL, or `about:blank` when a result carries none.
|
|
58
|
+
- **`test.title`** is the engine's own rule id. In ACT terms a rule is the *procedure* the implementation ran, which is exactly what a rule id names.
|
|
59
|
+
- **`test.isPartOf`** lists the Success Criteria that rule maps to, as `WCAG2:<criterion-id>`. Omitted entirely for a rule claiming no criterion — `aria-allowed-role` is the engine's one automatic rule in that position, and asserting an empty list would read as "maps to nothing we could find" rather than "deliberately maps to none".
|
|
60
|
+
- **`assertedBy`** and **`mode`** appear only when you supply them.
|
|
61
|
+
|
|
62
|
+
Criterion ids are derived from the criterion's own title (`Non-text Content` → `non-text-content`). `normativeMappings` also carries Understanding-document references and non-WCAG standards, which share `standard: "WCAG"` and a `requirement` with the real thing; a Success Criterion is the entry that states a conformance level and claims no other document type, and only those are read.
|
|
63
|
+
|
|
64
|
+
## Outcomes
|
|
65
|
+
|
|
66
|
+
| Engine | EARL |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `pass` | `earl:passed` |
|
|
69
|
+
| `fail` | `earl:failed` |
|
|
70
|
+
| `cantTell` | `earl:cantTell` |
|
|
71
|
+
| `notApplicable` | `earl:inapplicable` |
|
|
72
|
+
|
|
73
|
+
`earl:untested` has no counterpart: a rule that did not run produces no result to assert on, so it contributes no assertion rather than an untested one.
|
|
74
|
+
|
|
75
|
+
**`cantTell` does not cost conformance credit.** ACT's own consistency rules allow an automated implementation to report "cannot tell" on some — though not all — examples and still count as consistent. What a partially consistent implementation may *not* do is produce a false positive: failing an example the rule says should pass, or that is inapplicable. That is the gate worth watching, and it is a property of the rules rather than of this reporter. As of the last full run the engine produces **zero false positives** across the 798 ACT examples covering its 58 matched rules — see [`ACT_RULE_MAPPING.md`](./ACT_RULE_MAPPING.md), and re-run `scripts/act-testcase-check.js` for the live figure rather than trusting this one indefinitely.
|
|
76
|
+
|
|
77
|
+
## Several results, one report
|
|
78
|
+
|
|
79
|
+
`renderEarlReport` takes an array as readily as a single result, because a report covering many pages is the normal case:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
renderEarlReport([homeResult, checkoutResult, searchResult], { assertor });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Results sharing a URL merge into one subject — a caller scanning the same page under different `engineOptions` is still describing one resource, and the context has no way to express two subjects with the same source. Where two results assert on the same rule for the same URL, the last one wins.
|
|
86
|
+
|
|
87
|
+
Output is deterministic: subjects sort by source, assertions by rule id, and the same inputs produce byte-identical output in any order. That is what makes a diff between two engine versions meaningful.
|
|
88
|
+
|
|
89
|
+
## Options
|
|
90
|
+
|
|
91
|
+
| Option | Meaning |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `assertor` | `{ name, version }`. Defaults the name to `surea11y`; pass `null` to omit `assertedBy` entirely. |
|
|
94
|
+
| `mode` | An EARL test mode such as `'earl:automatic'`. Omitted when not supplied. |
|
|
95
|
+
|
|
96
|
+
## See also
|
|
97
|
+
|
|
98
|
+
- [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) — the result this reads
|
|
99
|
+
- [`ACT_RULE_MAPPING.md`](./ACT_RULE_MAPPING.md) — which ACT rules the engine's rules correspond to
|
|
100
|
+
- [`API_STABILITY.md`](./API_STABILITY.md) — what is covered by semver
|
package/docs/ENGINE_OPTIONS.md
CHANGED
|
@@ -55,10 +55,34 @@ Since versions are cumulative (2.1 = 2.0 + new; 2.2 = 2.0 + 2.1 + new), select a
|
|
|
55
55
|
{ tags: ['wcag22a', 'wcag22aa', 'wcag22aaa'] }
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
**One SC goes the other way.** WCAG 2.2 removed SC 4.1.1 Parsing — the only criterion ever dropped rather than added. A rule mapped to it carries its 2.0-origin tag (`wcag2a`) like any other baseline rule, plus `wcag22-removed`, and the version tag sets above therefore include it under a 2.2 target, where it does not belong.
|
|
58
|
+
**One SC goes the other way.** WCAG 2.2 removed SC 4.1.1 Parsing — the only criterion ever dropped rather than added. A rule mapped to it carries its 2.0-origin tag (`wcag2a`) like any other baseline rule, plus `wcag22-removed`, and the version tag sets above therefore include it under a 2.2 target, where it does not belong.
|
|
59
|
+
|
|
60
|
+
You do not have to do anything about that. The engine resolves a **target WCAG version** for every run and, when that target is 2.2, a `wcag22-removed` rule cannot report `fail`: it still runs, still reports every occurrence it found, but its outcome is coerced to `cantTell` and the result carries a `wcagVersionScope` field saying why (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md#a-check-result-checksresultsi)). Nothing is silently dropped, and a 2.2 run is not gated by a criterion 2.2 does not contain.
|
|
61
|
+
|
|
62
|
+
The target version is resolved in this order:
|
|
63
|
+
|
|
64
|
+
1. `engineOptions.wcagVersion` — `'2.0'`, `'2.1'` or `'2.2'`, if you set it.
|
|
65
|
+
2. The version-origin tags in your own filter: a set topping out at `wcag21a`/`wcag21aa` reads as a 2.1 target, one containing any `wcag22*` tag as 2.2, one with only `wcag2*` tags as 2.0. Only those nine tags count — an SC tag (`wcag411`) or `best-practice` says nothing about a version.
|
|
66
|
+
3. Otherwise `'2.2'`, this engine's default target.
|
|
59
67
|
|
|
60
68
|
```js
|
|
61
|
-
//
|
|
69
|
+
// Nothing to declare: a plain run already targets 2.2, so a duplicate id
|
|
70
|
+
// comes back cantTell rather than fail.
|
|
71
|
+
runDomRulesInPage(url, null, {}, null);
|
|
72
|
+
|
|
73
|
+
// Conformance-testing against 2.1, where SC 4.1.1 still exists:
|
|
74
|
+
runDomRulesInPage(url, null, { wcagVersion: '2.1' }, null);
|
|
75
|
+
|
|
76
|
+
// Same thing, implied by the tag set — no extra option needed:
|
|
77
|
+
runDomRulesInPage(url, null, {}, { tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The resolved target is reported back on every result as `engine.wcagVersion`, so you can confirm which one a run actually used.
|
|
81
|
+
|
|
82
|
+
If you would rather not see the rule at all under 2.2, exclude it outright — the tag is still there for exactly that:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
// WCAG 2.2 AA conformance, with the removed criterion left out entirely:
|
|
62
86
|
{
|
|
63
87
|
tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'],
|
|
64
88
|
excludeTags: ['wcag22-removed']
|
|
@@ -67,6 +91,78 @@ Since versions are cumulative (2.1 = 2.0 + new; 2.2 = 2.0 + 2.1 + new), select a
|
|
|
67
91
|
|
|
68
92
|
`duplicate-id` is the only rule carrying that tag today. Left in, it still reports something real — a duplicate id breaks `<label for>`, fragment links and `getElementById` whatever the standard says — it just is not a 2.2 conformance failure.
|
|
69
93
|
|
|
94
|
+
### Conformance profiles
|
|
95
|
+
|
|
96
|
+
`engineOptions.profile` names a conformance target instead of spelling out its tag set:
|
|
97
|
+
|
|
98
|
+
| Profile | Runs the rules tagged | WCAG target | Why |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| `wcag22-aa` | `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`, `wcag22a`, `wcag22aa` | 2.2 | WCAG 2.2 Level A and AA |
|
|
101
|
+
| `en301549-v4.1.1` | same as `wcag22-aa` | 2.2 | EN 301 549 V4.1.1 chapter 9 restates WCAG 2.2 A and AA |
|
|
102
|
+
| `en301549-v3.2.1` | `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa` | 2.1 | EN 301 549 V3.2.1 chapter 9 restates WCAG 2.1 A and AA, including 4.1.1 Parsing |
|
|
103
|
+
| `section508` | `wcag2a`, `wcag2aa` | 2.0 | The Revised 508 Standards incorporate WCAG 2.0 A and AA |
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
runDomRulesInPage(url, null, { profile: 'en301549-v3.2.1' }, null);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A standard registered as a profile under `profiles/` brings its own profiles to this list ([`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md#adding-another-standard)).
|
|
110
|
+
|
|
111
|
+
The WCAG target follows from the tags the same way it does for a hand-written set (see [Filtering by WCAG version](#filtering-by-wcag-version-21-vs-22)), so under `en301549-v3.2.1` a duplicate id can still `fail`, and under `en301549-v4.1.1` it cannot. Names are matched case-insensitively. A run that used a profile reports it back as `engine.profile`.
|
|
112
|
+
|
|
113
|
+
Precedence: anything that *includes* rules selects them instead of the profile — a `runOnly` with `tags`, `includeRuleIds` or `includeTestIds`, or an `include` in `engineOptions.rules`/`.tags`/`.tests`. Excludes still apply on top of the profile, whether they come from `runOnly` (`excludeTags`, `excludeRuleIds`, `excludeTestIds`, so a binding's `disableTags()` narrows the profile rather than replacing it) or, when there is no `runOnly` filter, from `engineOptions` (`tags.exclude`, `rules.exclude`, `tests.exclude`), and an explicit `engineOptions.wcagVersion` still wins over the version the profile implies. A profile that does not take effect — an unknown name, or one overridden as above — is not an error: the run proceeds as if none was given, logs a `console.warn` saying why, and carries no `engine.profile`.
|
|
114
|
+
|
|
115
|
+
A standard's profile may also leave rules out, when its standard waives a WCAG criterion or replaces a WCAG check with its own (`exclude` in the registry, [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md#adding-another-standard)): those rules and their WCAG rollups do not run, the rule catalog leaves them out too, and `engine.profileExcludes` names what was left out. No built-in profile excludes anything today.
|
|
116
|
+
|
|
117
|
+
A profile only chooses which rules run. It says nothing about whether passing them meets the standard it is named after: most Success Criteria need human judgement no automated rule covers (see [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md#what-this-engine-cannot-tell-you)).
|
|
118
|
+
|
|
119
|
+
An EN 301 549 profile also switches on the EN 301 549 clauses of the version it targets, as if `mappings: ['en301549:V4.1.1']` (or `V3.2.1`) had been passed; see the next section. Any standard's profile switches on its own version the same way. A profile with `mappedRules` also runs, whatever their tags, the rules its standard is mapped to: the ones with no WCAG mapping, such as `heading-order` and `skip-link`, have no WCAG tag to be selected by.
|
|
120
|
+
|
|
121
|
+
### Opt-in rules
|
|
122
|
+
|
|
123
|
+
Some rules check a standard's own requirements, ones WCAG does not make: a national standard may require a doctype, or forbid presentational attributes such as `bgcolor`. A `fail` from such a rule is a failure of that standard, not of WCAG, so these rules are **off by default**. Each carries its standard's rule tag and runs only when the selection asks for it:
|
|
124
|
+
|
|
125
|
+
- through the standard's profile, which lists the tag,
|
|
126
|
+
- by that tag (`tags: { include: '<tag>' }`, alone or with others), or
|
|
127
|
+
- by its id (`rules: { include: '<rule id>' }`, or `runOnly.includeRuleIds`),
|
|
128
|
+
- by the id of one of its standard's own rollups that groups it (`rules: { include: '<rollup id>' }` runs that rollup's rules, opt-in or not), or
|
|
129
|
+
- by unlocking it with `engineOptions.optInRules`, below.
|
|
130
|
+
|
|
131
|
+
Nothing else selects one: not a default run, not a WCAG tag set, not a WCAG or EN 301 549 profile, not a WCAG rollup id. A standard's own rollups carry the same tag and follow the same rule. Excludes apply to them as to any rule. This holds for a rule added through `customRules` that carries the tag too. The tags come from `ruleTag` in `src/coverage/standards.js`, one per registered standard that has rules of its own; core's built-in standards have none.
|
|
132
|
+
|
|
133
|
+
The tag selects the opt-in rules and nothing more. `tags: { include: '<tag>' }` alone runs that standard's own rules, not the WCAG and best-practice rules it is also mapped to, so it answers "what does the standard require beyond WCAG" and is not an audit against it. Its own rollups report `cantTell` for any requirement whose rules did not run (`missingChild`). For an audit, use the standard's profile.
|
|
134
|
+
|
|
135
|
+
#### Running every rule (`optInRules`)
|
|
136
|
+
|
|
137
|
+
To run every rule the engine has, whatever standard it belongs to, leave out the profile and unlock the opt-in rules:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
runDomRulesInPage(url, null, { optInRules: 'all' }, null);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`'all'` unlocks every standard's opt-in rules; a list of tags (`['<tag>']`, or `'<tag>'`) unlocks only those. It lifts the gate and nothing else: the rest of the selection still decides which rules run. So with no filter and no profile, as above, the run covers every rule and every standard's rollups; under `profile: 'wcag22-aa'` it still runs the WCAG rules only, since opt-in rules carry no WCAG tag, and under a standard's own profile it changes nothing for that standard's rules. Excludes apply as usual. A tag that is not an opt-in tag is ignored with a `console.warn`.
|
|
144
|
+
|
|
145
|
+
This is for seeing everything the engine can report, during development or when exploring a site. It is not a conformance target: such a run fails a page for things WCAG allows and only another standard forbids. The result says so: `engine.optInRules` lists the tags whose rules ran, and the HTML report, SARIF and JUnit show it next to the profile. It adds no other standard's numbers to results; pass `mappings` for that, for example `{ optInRules: 'all', mappings: ['<key>'] }` to see a standard's requirements each rule checks.
|
|
146
|
+
|
|
147
|
+
### Other standards (`mappings`)
|
|
148
|
+
|
|
149
|
+
Every result's `meta.normativeMappings` names the WCAG Success Criteria it tests. `engineOptions.mappings` adds the requirement of another standard that corresponds to each one. By default it adds none: a clause of a standard you do not audit against is noise in every SARIF tag, JUnit property and report.
|
|
150
|
+
|
|
151
|
+
| Value | Adds |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `'en301549'` | The EN 301 549 chapter 9 clause for each criterion, in every version that has it (V3.2.1 and V4.1.1) |
|
|
154
|
+
| `'en301549:V3.2.1'`, `'en301549:V4.1.1'` | The same, for that version only |
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
runDomRulesInPage(url, null, { mappings: ['en301549:V3.2.1'] }, null);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A standard registered as a profile adds its own key (and `<key>:<version>`), with the requirements it maps each rule to.
|
|
161
|
+
|
|
162
|
+
It takes an array or a comma-separated string, names and versions matched case-insensitively. What it adds to comes from the profile too: under `profile: 'en301549-v4.1.1'` the V4.1.1 clauses are there without asking, and `mappings` can add more. A name or version the engine has no table for is ignored with a `console.warn`. A run that carries any other standard reports which in `engine.mappings`, canonically spelled (`["en301549:V3.2.1"]`, or `["en301549"]` for every version).
|
|
163
|
+
|
|
164
|
+
It changes only what a result names, never which rules run or their outcomes. The rule catalog follows the same option: `getChecksCatalog(engineOptions)`, `getCheckDefById(ruleId, engineOptions)`, `getChecksForRunOnly(runOnly, engineOptions)`, `getRulesCatalog(engineOptions)` and `getCompositeRuleById(id, engineOptions)` name the standards a scan with those options would, a profile included when it would apply, so a catalog entry and a result always agree. With no options they name WCAG only; pass every standard's key in `mappings` for all of them. The published tables (`@surea11y/core/en301549`) are not filtered. A rule added through `customRules` keeps exactly the mappings it declares, whatever this option says.
|
|
165
|
+
|
|
70
166
|
### Via `engineOptions` (no `runOnly`)
|
|
71
167
|
|
|
72
168
|
Same filtering, expressed as comma-separated strings (or arrays) nested in `engineOptions`:
|
|
@@ -86,6 +182,10 @@ runDomRulesInPage(url, null, {
|
|
|
86
182
|
```js
|
|
87
183
|
const engineOptions = {
|
|
88
184
|
locale: 'en', // default 'en'; de-DE falls back to de, then to en per string
|
|
185
|
+
wcagVersion: '2.2', // default '2.2' — the conformance target, see "Filtering by WCAG version" above
|
|
186
|
+
profile: 'en301549-v4.1.1', // optional — a named conformance target, see "Conformance profiles" above
|
|
187
|
+
mappings: ['en301549'], // optional — standards besides WCAG to name on each result, see "Other standards" above
|
|
188
|
+
optInRules: 'all', // optional — also run rules other standards add beyond WCAG, see "Running every rule" above
|
|
89
189
|
messages: { de: { /* key: text */ } }, // optional caller-supplied dictionaries; win over built-in ones
|
|
90
190
|
includeHiddenElements: false, // default false — set true to evaluate hidden/collapsed subtrees too
|
|
91
191
|
includeShadowDom: true, // default true — opt OUT with `false` to skip open shadow roots
|
|
@@ -131,6 +231,10 @@ const engineOptions = {
|
|
|
131
231
|
| Option | Meaning |
|
|
132
232
|
|---|---|
|
|
133
233
|
| `locale` | Any string. A code with a subtag falls back to its base language first, so `de-DE` uses `de`; failing that, English. Individual strings then fall back the same way (chosen locale → `en` → the rule's literal English text), so a partly-translated locale never produces missing text. All of that is silent in the strings themselves, so the result reports what actually happened in `engine.locale` — check it if you need to know whether you got the language you asked for. See [`I18N.md`](./I18N.md). |
|
|
234
|
+
| `wcagVersion` | `'2.0'`, `'2.1'` or `'2.2'` — which version of WCAG the run is conformance-testing against. Defaults to whatever your version-origin tags imply, and to `'2.2'` when they imply nothing. The only thing it currently changes is SC 4.1.1 Parsing, removed in 2.2: under a 2.2 target a rule tagged `wcag22-removed` still runs and still reports its occurrences, but cannot `fail` — see ["Filtering by WCAG version"](#filtering-by-wcag-version-21-vs-22) above. Any other value is ignored and the default applies. |
|
|
235
|
+
| `profile` | Optional named conformance target: `'wcag22-aa'`, `'en301549-v4.1.1'`, `'en301549-v3.2.1'`, `'section508'`, or a registered standard's own. Selects rules by the matching tag set when nothing else includes any, and the WCAG target follows from those tags. Reported back as `engine.profile` when it took effect; otherwise ignored with a console warning. See ["Conformance profiles"](#conformance-profiles) above. |
|
|
236
|
+
| `mappings` | Optional standards besides WCAG whose requirements `meta.normativeMappings` names: `'en301549'`, `'en301549:V3.2.1'`, `'en301549:V4.1.1'`, or a registered standard's key, as an array or comma-separated string. Default none, so results name WCAG only; an EN 301 549 profile adds its own version. Reported back as `engine.mappings` when any applies. See ["Other standards"](#other-standards-mappings) above. |
|
|
237
|
+
| `optInRules` | Optional: `'all'`, or a list of opt-in rule tags, as an array or comma-separated string. Unlocks those rules outside their standard's profile; the rest of the selection still decides what runs. Reported back as `engine.optInRules` when it ran a rule the selection would not have run otherwise; unknown tags are ignored with a console warning. See ["Running every rule"](#running-every-rule-optinrules) above. |
|
|
134
238
|
| `messages` | Optional `{ [locale]: { key: text } }`. Checked before the engine's own tables, so it can override individual strings or supply a language the build does not carry. Keys you omit fall back normally, so a partial override is fine. This is how the standalone browser bundle receives a locale side file, and it is the only way to get a dictionary into a page context, since the in-page runner is serialized and cannot read files. See [`I18N.md`](./I18N.md). |
|
|
135
239
|
| `includeHiddenElements` | Default `false`: helper queries exclude elements hidden by structural/CSS mechanisms such as `display:none`, `[hidden]`, closed `<details>`, and hidden rendering-only host elements (with descendants excluded too). Set `true` to include those hidden/collapsed subtrees in evaluation (legacy/static-markup behavior). |
|
|
136
240
|
| `includeShadowDom` | Default `true`: rules using `helpers.queryAllSmart` traverse into open shadow roots. Set `false` to scan only the light DOM. Closed shadow roots are never reachable either way (no DOM API exposes them). |
|
|
@@ -141,9 +245,9 @@ const engineOptions = {
|
|
|
141
245
|
| `contrast.rootCanvasFallback` | The assumed page background color when it's not computable at all — only matters in `auditorAssist` mode. |
|
|
142
246
|
| `visibilityMode` | Controls how strict the three contrast rules (`contrast-minimum`, `contrast-enhanced`, `contrast-computable`) are about deciding a text node is actually eligible to check. **Not read by any other rule.** `'styleOnly'` (default): eligibility is CSS-only — `display`, `visibility`, `opacity`, ancestor-hiding, etc. `'styleAndGeometry'`: adds real layout checks (`getClientRects()`/`getBoundingClientRect()`) on top of that — text with no client rects, or zero width/height, is excluded too. Reach for `'styleAndGeometry'` when running under a real browser/Playwright-Puppeteer (`runa11yCoreInPage`) and you want contrast findings to reflect actual rendered layout rather than just computed style; under plain jsdom (`runDomRulesInPage`) there's no real layout engine, so `'styleAndGeometry'` mostly just adds `getBoundingClientRect()` zero-size checks, not true clipping/overflow detection — see [`LIMITATIONS.md`](./LIMITATIONS.md). |
|
|
143
247
|
| `policyContract` / `policy` | See [`POLICY.md`](./POLICY.md) — controls which outcomes/confidence values are allowed and whether manual rules' would-be `fail`s get coerced to `cantTell`. |
|
|
144
|
-
| `output.includeSelector` / `.includeHtml` | Suppresses the engine's automatic `selector`/`html` fill-in. Since every rule was migrated to report its element rather than build occurrences by hand (1.5.0), that fill-in is the path almost all of them take: setting `includeSelector: false` strips selectors from
|
|
145
|
-
| `rules[ruleId]` | Passed through to that rule as `ctx.config`, and — for `excludeSelectors` specifically — read by the engine itself before the rule ever runs. See "Rule-scoped `excludeSelectors`" below. Any other key is passthrough only: **no shipped rule
|
|
146
|
-
| `probes` | An optional, JSON-safe evidence object your host application
|
|
248
|
+
| `output.includeSelector` / `.includeHtml` | Suppresses the engine's automatic `selector`/`html` fill-in. Since every rule was migrated to report its element rather than build occurrences by hand (1.5.0), that fill-in is the path almost all of them take: setting `includeSelector: false` strips selectors from nearly every rule, and `includeHtml: false` HTML snippets. The remainder still assemble those fields themselves inside `runInPage` and are unaffected — among them `contrast-minimum`/`contrast-enhanced` (whose findings are text runs, not elements), `page-title-present` and `identical-links-same-purpose`. So this narrows output substantially but is still not a guarantee of *no* selectors or HTML anywhere in the result. |
|
|
249
|
+
| `rules[ruleId]` | Passed through to that rule as `ctx.config`, and — for `excludeSelectors` specifically — read by the engine itself before the rule ever runs. See "Rule-scoped `excludeSelectors`" below. Any other key is passthrough only: **no shipped rule takes anything else from it.** A rule's declared settings, such as `contrast-minimum`'s thresholds, are its standard's, so a caller's value for one is dropped: a result naming WCAG 1.4.3 is always decided at 4.5:1. A standard with other thresholds has its own rule, a variant ([`RULE_AUTHORING.md`](./RULE_AUTHORING.md#rule-variants)). |
|
|
250
|
+
| `probes` | An optional, JSON-safe evidence object your host application supplies, for what a scan of one page cannot see, such as the site's other pages. The engine caps it before rules read it (`ctx.inputs.probes`): six levels deep, 200 items per array, 50 keys per object, 2,000 characters per string. `crawl.pageTitles` (`{ pages: [{ url, title }] }`) is read by `page-title-patterns`, to look for generic and templated titles across a site. A profile's rules may read probes of their own, which its documentation describes. |
|
|
147
251
|
| `perfStats` / `profileRules` | Debug-only. `perfStats: true` returns internal counters on the result's `perfStats` field; `profileRules: true` **additionally** adds a per-rule timing breakdown there. `profileRules` on its own does nothing — `perfStats` is what creates the object the breakdown lives in. Shape is not part of the stable output contract — don't build on it. Note also that `profileRules` is the one option that makes output non-deterministic: counters are stable across identical runs, wall-clock timings are not. Leave it off if you diff results between runs. |
|
|
148
252
|
| `pingWaitTime` / `frameWaitTime` | Only read by `runa11yCoreAcrossFrames` (see [`INTEGRATION.md`](./INTEGRATION.md#cross-frame-scanning-including-cross-origin)) — how long to wait for a child frame to answer a ping (default `500`ms) and a full run request (default `60000`ms) before treating it as unreachable. Ignored by `runDomRulesInPage`/`runa11yCoreInPage`. |
|
|
149
253
|
|
package/docs/I18N.md
CHANGED
|
@@ -4,16 +4,23 @@ Every rule's title/description, and every occurrence's summary/hint, is localize
|
|
|
4
4
|
|
|
5
5
|
## Current locale coverage
|
|
6
6
|
|
|
7
|
-
| Locale |
|
|
7
|
+
| Locale | Files | Keys | Values identical to English |
|
|
8
8
|
|---|---|---|---|
|
|
9
|
-
| `en` (English) | `src/i18n/en.json` |
|
|
10
|
-
| `fr` (French) | `src/i18n/fr.json` |
|
|
11
|
-
| `de` (German) | `src/i18n/de.json` |
|
|
12
|
-
| `es` (Spanish) | `src/i18n/es.json` |
|
|
9
|
+
| `en` (English) | `src/i18n/en.json` | 841 | — (the canonical/fallback set) |
|
|
10
|
+
| `fr` (French) | `src/i18n/fr.json` | 841 | 3 — *Orientation*, *Interruptions*, *Occurrences*, which are the same words in French |
|
|
11
|
+
| `de` (German) | `src/i18n/de.json` | 841 | 0 |
|
|
12
|
+
| `es` (Spanish) | `src/i18n/es.json` | 841 | 2 — *Selector*, the same word in Spanish |
|
|
13
|
+
| `ja` (Japanese) | `src/i18n/ja.json` | 841 | 0 |
|
|
14
|
+
|
|
15
|
+
The last column is what `npm run i18n:report` measures: values that match the English text character for character. That catches a key nobody has translated yet, but it also counts a word that is simply the same in both languages, and it cannot see an English word left inside an otherwise translated sentence. Those are found by reading the text; none remain in the locales above.
|
|
13
16
|
|
|
14
17
|
Locale files are plain JSON: a flat map of key to translated string, in the same key order as `en.json`. Nothing else lives in them, so contributing a language means editing text and never touching code.
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
A locale's text may be split between several files. `src/i18n/<locale>.json` holds the engine's own messages; a profile's `profiles/<key>/i18n/<locale>.json` holds those of its rules (see [`profiles/README.md`](../profiles/README.md)). Each file is synced against the `en.json` next to it, and the build merges them into one dictionary per locale. A key lives in exactly one of them: the build fails if two files define it.
|
|
20
|
+
|
|
21
|
+
A profile chooses its languages: the locale files in its `i18n/` folder are the list. It always has `en.json`; a locale core has and the profile does not shows that profile's messages in English, as the engine does for any key a locale lacks. Because the profile chose it, `engine.locale.reason` stays `ok`: the build records the keys each locale leaves out that way, and only a key missing otherwise makes it `partial-dictionary`. Core's own messages are never left out by choice, so a locale only a profile has is reported as `partial-dictionary`.
|
|
22
|
+
|
|
23
|
+
Every locale file carries every key the `en.json` next to it has. Keeping it that way is the job of `npm run i18n:sync`: run it after any change to `en.json` and it rewrites every non-English locale file to match — adding keys that are new, dropping keys `en.json` no longer has, and leaving existing translations alone. A key it adds is seeded with the English text, which counts as untranslated until someone replaces it.
|
|
17
24
|
|
|
18
25
|
Forget to run it and the build fails: `tests/i18n-sync.test.js` and `tests/i18n/i18n-locale-completeness.test.js` reject a missing key, an orphaned key, or any file that `i18n:sync` would rewrite. `npm run i18n:check` reports the same thing without touching the files, and `npm run i18n:report` prints per-locale coverage.
|
|
19
26
|
|
|
@@ -55,7 +62,7 @@ Graceful fallback has one drawback: ask for a language the build doesn't carry a
|
|
|
55
62
|
"engine": {
|
|
56
63
|
"tag": "a11ycore",
|
|
57
64
|
"schemaVersion": "1.0.0",
|
|
58
|
-
"locale": { "requested": "
|
|
65
|
+
"locale": { "requested": "ko", "resolved": "en", "reason": "unknown-locale" }
|
|
59
66
|
}
|
|
60
67
|
```
|
|
61
68
|
|
|
@@ -66,8 +73,8 @@ Graceful fallback has one drawback: ask for a language the build doesn't carry a
|
|
|
66
73
|
| `ok` | You got exactly what you asked for, and that dictionary carries every key. |
|
|
67
74
|
| `primary-subtag` | Your code carried a subtag with no dictionary of its own, so its base language was used — you asked for `de-DE` and got `de`. Normal and expected; nothing to fix. A difference in case alone is not this: `DE` reports `ok`. |
|
|
68
75
|
| `dictionary-not-loaded` | The project ships that language, but this copy of the engine doesn't carry it and none was supplied. In practice: the standalone browser bundle without its locale side file. |
|
|
69
|
-
| `unknown-locale` | The project has no such translation at all, so English was used. `
|
|
70
|
-
| `partial-dictionary` | The dictionary was used but is missing some keys, so those individual strings fell back to English. |
|
|
76
|
+
| `unknown-locale` | The project has no such translation at all, so English was used. `ko` and `pt-BR` both land here today. |
|
|
77
|
+
| `partial-dictionary` | The dictionary was used but is missing some keys, so those individual strings fell back to English. A profile's messages in a language the profile does not offer also show in English, but do not count here. |
|
|
71
78
|
|
|
72
79
|
Treat the list as open — a later release can add a value, so match on the ones you care about and let the rest fall through a default.
|
|
73
80
|
|
|
@@ -83,11 +90,14 @@ Which languages are available depends on how you load the engine.
|
|
|
83
90
|
| A binding (Playwright, Cypress, …) | Every locale, built in. Nothing to configure. |
|
|
84
91
|
| `surea11y.browser.js` in a `<script>` tag | English. Load `surea11y.i18n.<locale>.js` after it for anything else. |
|
|
85
92
|
|
|
86
|
-
The bundle is split because it travels over the network to every page that uses it, and no page needs
|
|
93
|
+
The bundle is split because it travels over the network to every page that uses it, and no page needs every language. Keeping English inline and the rest optional took about 280 KB off the download and stops it growing as languages are added. Nothing else changes: the Node package and the bindings are unaffected.
|
|
87
94
|
|
|
88
95
|
```html
|
|
89
96
|
<script src="surea11y.browser.js"></script>
|
|
90
|
-
<script src="surea11y.i18n.
|
|
97
|
+
<script src="surea11y.i18n.ja.js"></script>
|
|
98
|
+
<script>
|
|
99
|
+
const result = a11ycore.runa11yCoreInPage(location.href, null, { locale: 'ja' }, null);
|
|
100
|
+
</script>
|
|
91
101
|
```
|
|
92
102
|
|
|
93
103
|
Ask for a language whose file you didn't load and you get English, with `engine.locale.reason` set to `dictionary-not-loaded` — different from `unknown-locale`, which means the project has no such translation at all.
|
|
@@ -105,11 +115,35 @@ runDomRulesInPage(url, null, {
|
|
|
105
115
|
|
|
106
116
|
Keys you don't supply fall back normally, so a partial override is fine.
|
|
107
117
|
|
|
118
|
+
## Notes on individual locales
|
|
119
|
+
|
|
120
|
+
**Japanese (`ja`).** Terms that WCAG 2.2 defines follow the Japanese translation published by WAIC (Web Accessibility Infrastructure Committee): 達成基準 for success criterion, アクセシブルな名前 for accessible name, テキストによる代替 for text alternative, 支援技術 for assistive technology, and the WAIC names for the criteria themselves, such as ラベルを含む名前 (name), コントラスト (最低限) and ターゲットのサイズ (最低限). Messages use the です/ます style, and hints ask for a fix with 〜してください. A few conventions worth keeping when you edit it:
|
|
121
|
+
|
|
122
|
+
- Where an English title says *should*, the Japanese one ends in 〜が望ましい (or says 推奨), and *(manual review)* becomes (手動確認). A title that says *must* is phrased as a requirement. Keep that split: it is how a reader tells advisory findings from confirmed failures.
|
|
123
|
+
- A `cantTell` summary says what a person has to decide, usually ending in 人による確認が必要です or in a sentence stating what could not be determined. It never reads as a failure.
|
|
124
|
+
- A `pass` message describes the check that ran (計算できたすべてのテキストが…基準値を満たしています), not the page. Nothing in the dictionary claims conformance.
|
|
125
|
+
- Quoted page content uses 「…」, so `{{name}}` becomes 「{{name}}」. Code keeps its ASCII quotes: `alt=""`, `role="img"`.
|
|
126
|
+
- Rule descriptions quote the phrases a rule matches in both languages (「こちら」、"click here"), since the rules recognize both.
|
|
127
|
+
|
|
128
|
+
Some parameter values are CSS or attribute vocabulary and are interpolated as-is in every locale: `{{fontWeightLabel}}` (`bold`, `700`), `{{labelSource}}` and `{{nameMechanism}}` (`aria-label`, `title`).
|
|
129
|
+
|
|
130
|
+
## Language-specific matching
|
|
131
|
+
|
|
132
|
+
Five rules judge text by known phrases, so they carry word lists as well as messages: `link-name-quality` ("click here", 「こちら」), `heading-quality` ("Untitled", 「見出し」), `form-control-label-quality` ("Label", 「入力欄」), `page-title-patterns` ("Home", 「トップページ」) and `media-alternative-transcript-evidence` ("transcript", 「文字起こし」). Each list covers `en`, `de`, `es`, `fr` and `ja`.
|
|
133
|
+
|
|
134
|
+
English is always checked. The list for the element's own language, taken from the nearest `lang` attribute (the `<html>` element's for a page title), is added on top. Checking every list everywhere would flag words that are generic in one language and a real name in another, such as "Suite" or "Plus" on an English page. Transcript words are the exception: they are distinctive in every language and a page can link a transcript in another language, so all of them are always checked. Text is NFKC-normalized first, so full-width forms such as 「見出し2」 match.
|
|
135
|
+
|
|
136
|
+
`page-title-patterns` also counts each Chinese, Japanese or Korean character as two when deciding whether a title is too short, since 「お問い合わせ」 is a complete title in six characters.
|
|
137
|
+
|
|
138
|
+
Adding a language means adding its words to these lists as well as translating its dictionary.
|
|
139
|
+
|
|
108
140
|
## Where keys are used
|
|
109
141
|
|
|
110
142
|
Two independent key namespaces, both resolved the same way:
|
|
111
143
|
|
|
112
144
|
- **Rule-level**: `meta.i18n.titleKey` / `meta.i18n.descriptionKey` — resolve a rule's `title`/`description` on every `checksResults[]` entry.
|
|
145
|
+
- **HTML report**: the `report_*` keys label the page `@surea11y/core/report` renders — headings, table columns, outcome and severity names, the headline, the pager. The report reads them in the locale the scan resolved to, so translating a dictionary translates the report too.
|
|
146
|
+
- **Composite-level**: `meta.titleKey` / `meta.descriptionKey` in `src/catalogs/composites.wcag.js` — resolve each `rulesResults[]` rollup's `title`/`description`. The catalog's own `title`/`description` must match the English value, which `tests/i18n/i18n-composite-titles.test.js` checks. Titles use each language's published name for the success criterion.
|
|
113
147
|
- **Occurrence-level**: `i18n.summaryKey` / `i18n.hintKey`, with `i18n.params` for `{{placeholder}}` interpolation — resolve an occurrence's `summary`/`hint`. See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md#an-occurrence-occurrencesi).
|
|
114
148
|
|
|
115
149
|
Both are included in the result alongside the already-resolved text, so you can re-render in a different locale from a saved result without re-scanning — see the `i18n` field's presence in `OUTPUT_SCHEMA.md`.
|
|
@@ -132,10 +166,18 @@ npm install
|
|
|
132
166
|
npm run i18n:new pt-BR
|
|
133
167
|
```
|
|
134
168
|
|
|
135
|
-
That writes `src/i18n/pt-BR.json
|
|
169
|
+
That writes `src/i18n/pt-BR.json`, containing every key `en.json` has, in the same order, seeded with the English text as a placeholder. **Use the shortest code that identifies the language** — `pt`, `nl`, `pl`. A file named `pt.json` serves everyone who asks for `pt`, `pt-BR` or `pt-PT`, because a code with a subtag falls back to its base language. Name it `pt-BR.json` and only people who ask for exactly that get it; `pt` speakers elsewhere fall through to English.
|
|
136
170
|
|
|
137
171
|
Add a regional file only when the wording genuinely has to differ, and add it alongside the base language rather than instead of it.
|
|
138
172
|
|
|
173
|
+
A profile's messages are a file of their own, added with `--profile`:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
npm run i18n:new -- pt-BR --profile <key>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
writes `profiles/<key>/i18n/pt-BR.json` the same way. Without it, that profile's rules show in English in that locale.
|
|
180
|
+
|
|
139
181
|
The command refuses to overwrite a file that already exists. To pick up work on an existing locale, edit it directly.
|
|
140
182
|
|
|
141
183
|
### 3. Translate the values
|
|
@@ -161,7 +203,7 @@ Four things to leave alone:
|
|
|
161
203
|
|
|
162
204
|
Write for someone fixing the page, not for a specialist: say what is wrong and what to do about it. Where your language has established accessibility vocabulary (a national WCAG translation, a government standard), follow it rather than inventing terms.
|
|
163
205
|
|
|
164
|
-
If a string is genuinely identical in your language, leave it.
|
|
206
|
+
If a string is genuinely identical in your language, leave it. The report counts it as identical to English, which is correct and needs no action.
|
|
165
207
|
|
|
166
208
|
### 4. Check your progress
|
|
167
209
|
|
|
@@ -169,7 +211,7 @@ If a string is genuinely identical in your language, leave it. It is counted as
|
|
|
169
211
|
npm run i18n:report
|
|
170
212
|
```
|
|
171
213
|
|
|
172
|
-
Prints, per locale, how many values differ from English, plus any missing or orphaned keys.
|
|
214
|
+
Prints, per locale, how many values differ from English, plus any missing or orphaned keys. A value that still matches the English text is counted as not yet translated. While you work, that is your progress signal; once you are done, whatever it still counts should be words your language shares with English.
|
|
173
215
|
|
|
174
216
|
You do not need every string on day one. Per-string fallback means an unfinished locale renders in your language where you have translated it and in English everywhere else, which is exactly how `fr`, `de` and `es` started. Ship what you have.
|
|
175
217
|
|
|
@@ -182,25 +224,25 @@ npm test
|
|
|
182
224
|
|
|
183
225
|
`npm run build` also emits `surea11y.i18n.<locale>.js` for the standalone browser bundle — generated, so there is nothing for you to write.
|
|
184
226
|
|
|
185
|
-
Then open a pull request touching `src/i18n/<locale>.json
|
|
227
|
+
Then open a pull request touching `src/i18n/<locale>.json` (and a profile's `profiles/<key>/i18n/<locale>.json` if you translated its messages too), plus one row in the coverage table at the top of this file. Tell us which language and, if you use one, which national terminology standard you followed — that helps whoever reviews it later.
|
|
186
228
|
|
|
187
229
|
Once a locale is in the repository it is maintained with the rest of the engine: when a new rule adds strings, `npm run i18n:sync` seeds them in your file in English and `npm run i18n:report` shows them as outstanding.
|
|
188
230
|
|
|
189
231
|
## Maintaining the locales
|
|
190
232
|
|
|
191
|
-
When you add, rename or remove a key in `src/i18n/en.json`:
|
|
233
|
+
When you add, rename or remove a key in `src/i18n/en.json` or a profile's `i18n/en.json`:
|
|
192
234
|
|
|
193
235
|
```sh
|
|
194
236
|
npm run i18n:sync
|
|
195
237
|
```
|
|
196
238
|
|
|
197
|
-
Every other locale is rewritten to match — new keys seeded in English, removed keys dropped, existing translations untouched. The command is idempotent and prints what it changed per
|
|
239
|
+
Every other locale file in the same folder is rewritten to match its `en.json` — new keys seeded in English, removed keys dropped, existing translations untouched. A locale a profile has no file for is left out: it never creates one. The command is idempotent and prints what it changed per file.
|
|
198
240
|
|
|
199
241
|
| Command | Does |
|
|
200
242
|
|---|---|
|
|
201
|
-
| `npm run i18n:new <locale>` | Create
|
|
202
|
-
| `npm run i18n:sync` | Bring every locale file back in line with `en.json`. Add `-- <locale>` to restrict it to one. |
|
|
243
|
+
| `npm run i18n:new <locale>` | Create core's file for the locale from `src/i18n/en.json`; with `-- <locale> --profile <key>`, a profile's from its own. Refuses to overwrite. |
|
|
244
|
+
| `npm run i18n:sync` | Bring every locale file back in line with its `en.json`. Add `-- <locale>` to restrict it to one. |
|
|
203
245
|
| `npm run i18n:check` | Same comparison, writes nothing, exits non-zero on drift. |
|
|
204
|
-
| `npm run i18n:report` |
|
|
246
|
+
| `npm run i18n:report` | Translation coverage per folder and locale: core's, then each profile's for the locales it has. |
|
|
205
247
|
|
|
206
248
|
`npm test` fails if a locale file has drifted, so an added key cannot reach `main` without every locale carrying it.
|
package/docs/INTEGRATION.md
CHANGED
|
@@ -167,6 +167,8 @@ Notes for CI specifically:
|
|
|
167
167
|
|
|
168
168
|
**How it works**: a parent frame's `runa11yCoreAcrossFrames()` call pings each direct child `<iframe>`/`<frame>` via `postMessage`; if — and only if — that child has *also* called `a11yCoreEnableFrameResponder()` (its own opt-in to being scannable from above), it runs its own scan and replies with the result, which the parent includes. **A non-cooperating frame (the common case for most third-party embeds you don't control) is simply unreachable** — the same-origin policy allows no way around it from inside the page.
|
|
169
169
|
|
|
170
|
+
**Who a responder answers**: the frame that embeds it, and nothing else. Enabling the responder is consent to be scanned *from above*, not by anything that can reach you — a sibling frame can obtain a reference through `parent.frames[i]` and `postMessage` to you across origins, and a scan result carries `occurrences[].html`, which is DOM content the same-origin policy otherwise makes unreadable to it. A `run` command whose sender is not the direct parent is ignored, as is one arriving at a window nothing embeds. Replies are matched the same way: only the frame a request was addressed to can answer it, so another window cannot settle a scan in flight by naming its id. The relay is hop-by-hop — a grandchild is reached through its own parent — so a legitimate request always arrives from the direct parent.
|
|
171
|
+
|
|
170
172
|
```js
|
|
171
173
|
// Inside the embedded/child page (e.g. a widget's own bundle), once, at load:
|
|
172
174
|
const { a11yCoreEnableFrameResponder } = require('@surea11y/core');
|
|
@@ -191,6 +193,6 @@ A few things worth knowing:
|
|
|
191
193
|
- **Async, unlike the other two runners** — `postMessage` round-trips can't be synchronous, so this is a separate, Promise-returning pair rather than an `engineOptions` flag on `runa11yCoreInPage` (which stays synchronous, unchanged, for every existing caller).
|
|
192
194
|
- **`engineOptions.pingWaitTime`** (default `500`ms) and **`engineOptions.frameWaitTime`** (default `60000`ms) control how long a child frame gets to answer a ping and a full run request respectively.
|
|
193
195
|
- **No jsdom/Node equivalent** — this is browser-only. jsdom's window/frame model doesn't meaningfully represent independent-realm cross-origin `postMessage`, and the feature has no purpose in Node anyway.
|
|
194
|
-
- **Bundler-free, like `runa11yCoreInPage`** —
|
|
195
|
-
- **Cost
|
|
196
|
+
- **Bundler-free, like `runa11yCoreInPage`** — raw-source injection (a bookmarklet, a content script with no build step) still works with no bundler, but the slice you inject has to start at the `// SELF-CONTAINED in-page runner` marker rather than at the cross-frame block. `runa11yCoreAcrossFrames` scans its own frame by calling `runa11yCoreInPage`, so the two travel together. Everything from that marker to `module.exports` is one contiguous chunk with no `require()` in it. If you *do* use a normal bundler/`require`/`import`, that works too, unchanged.
|
|
197
|
+
- **Cost**: these two functions used to carry their own private copy of the rule catalog and helpers, which put the catalog in `src/core.js` three times over and took the file to ~4.3MB. They share `runa11yCoreInPage`'s copy now, which brings it to ~2.67MB and leaves one copy to grow as rules are added.
|
|
196
198
|
- **No origin/identity check on the sender** beyond the message's own namespaced envelope. Running a read-only scan and replying with DOM-derived results isn't a privileged operation; the content involved is no more sensitive than what's already rendered on the page.
|
package/docs/JUNIT.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# JUnit report
|
|
2
|
+
|
|
3
|
+
`@surea11y/core/junit` renders a scan result as JUnit XML, the test report format CI dashboards read natively: GitLab's merge request test widget, Azure DevOps' Tests tab, Jenkins and CircleCI.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const { renderJunitReport } = require('@surea11y/core/junit');
|
|
7
|
+
const { runDomRulesInPage } = require('@surea11y/core');
|
|
8
|
+
|
|
9
|
+
const result = runDomRulesInPage(url, null, { profile: 'wcag22-aa' }, null);
|
|
10
|
+
require('fs').writeFileSync('surea11y.junit.xml', renderJunitReport(result));
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`renderJunitReport(result, options)` is a pure function: it returns the XML as a string and never touches the filesystem. It works just as well on a result saved as JSON earlier, such as the output of `surea11y scan <target> --json` from [`@surea11y/cli`](https://github.com/SureA11y/cli#readme) — see [`CI_INTEGRATIONS.md`](./CI_INTEGRATIONS.md#junit-test-reports).
|
|
14
|
+
|
|
15
|
+
## Shape
|
|
16
|
+
|
|
17
|
+
One `<testsuite>` per WCAG Success Criterion, one `<testcase>` per rule mapped to it:
|
|
18
|
+
|
|
19
|
+
```xml
|
|
20
|
+
<testsuites name="surea11y" tests="14" failures="2" errors="0" skipped="4" time="0">
|
|
21
|
+
<testsuite name="WCAG 1.1.1 Non-text content: text alternatives" tests="1" failures="1" errors="0" skipped="0" time="0">
|
|
22
|
+
<properties>
|
|
23
|
+
<property name="wcagCriterion" value="1.1.1"/>
|
|
24
|
+
<property name="wcagLevel" value="A"/>
|
|
25
|
+
<property name="en301549" value="9.1.1.1"/>
|
|
26
|
+
<property name="criterionOutcome" value="fail"/>
|
|
27
|
+
<property name="engine" value="a11ycore"/>
|
|
28
|
+
<property name="schemaVersion" value="1.0.0"/>
|
|
29
|
+
<property name="wcagVersion" value="2.2"/>
|
|
30
|
+
<property name="profile" value="wcag22-aa"/>
|
|
31
|
+
<property name="locale" value="en"/>
|
|
32
|
+
<property name="url" value="https://example.test/"/>
|
|
33
|
+
</properties>
|
|
34
|
+
<testcase classname="wcag-1.1.1" name="img-alt-present" time="0">
|
|
35
|
+
<failure type="fail" message="1 failing occurrence: Missing alt attribute on <img>.">- Missing alt attribute on <img>. Add an alt attribute (use alt="" only for decorative images).
|
|
36
|
+
selector: html > body > main > img
|
|
37
|
+
html: <img src="a.png"></failure>
|
|
38
|
+
</testcase>
|
|
39
|
+
</testsuite>
|
|
40
|
+
</testsuites>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **Suites follow the criterion**, because that is what people track, and **testcases follow the rule** rather than the occurrence, so a defect repeated forty times on a page is one failing test whose body lists all forty, and test counts stay stable between runs.
|
|
44
|
+
- Suites come from each rule's own WCAG mappings, not from the composites, so every rule that ran is reported even when composites were excluded. The composite, when it ran, supplies the suite's title and the `criterionOutcome` property. A rule mapped to two criteria appears in both suites. A rule mapped to no criterion goes into a final `Other checks` suite with `classname="other"`.
|
|
45
|
+
- The run's `engine`, `schemaVersion`, `wcagVersion`, `profile`, `optInRules` (comma-separated tags, when `engineOptions.optInRules` added rules), `locale` and `url` are repeated as properties on every suite, each only when the result has it.
|
|
46
|
+
- Suites are ordered by criterion, numerically (1.4.3 before 1.4.10), and testcases by rule id.
|
|
47
|
+
- `en301549` properties name the EN 301 549 clause that restates the criterion, where there is one and the scan asked for EN 301 549 clauses (see [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md#en-301-549)).
|
|
48
|
+
|
|
49
|
+
## Outcomes
|
|
50
|
+
|
|
51
|
+
| Rule outcome | JUnit | Why |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `fail` | `<failure type="fail">`, one line per failing occurrence (message, hint, selector, markup) | The deterministic, gating case. |
|
|
54
|
+
| `cantTell` | `<skipped>` saying how many occurrences need manual review | JUnit has no "could not tell". Skipped surfaces it without turning a build red, the same line SARIF draws with `warning`. |
|
|
55
|
+
| `pass` | a bare `<testcase>` | |
|
|
56
|
+
| `notApplicable` | left out | A page has hundreds; none says anything. `includeNotApplicable: true` adds them as `<skipped message="Not applicable">`. |
|
|
57
|
+
|
|
58
|
+
A `fail` rule that also has `cantTell` occurrences reports the failures in `<failure>` and the undecided ones in `<system-out>`, where dashboards show test output. A skipped `cantTell` rule does the same.
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
| Option | Default | Effect |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `cantTellAs` | `'skipped'` | `'failure'` reports `cantTell` rules as `<failure type="cantTell">`, for a pipeline that must not pass while anything is undecided. |
|
|
65
|
+
| `includeNotApplicable` | `false` | Include `notApplicable` rules as skipped tests. |
|
|
66
|
+
| `baselineEntries` | none | Entries from [`BASELINE.md`](./BASELINE.md)'s `buildBaselineEntries()`. Fail occurrences recorded there are dropped, exactly as in SARIF. A rule whose every failure is already known is `<skipped message="N known failures recorded in the baseline">`, not passing: it did not pass. |
|
|
67
|
+
| `name` | `'surea11y'` | The `name` of the root `<testsuites>`, for telling several pages' reports apart in one dashboard. |
|
|
68
|
+
|
|
69
|
+
## Determinism
|
|
70
|
+
|
|
71
|
+
The engine has no clock, and this report does not invent one: every `time` is `"0"`, and a `timestamp` attribute appears on each suite only when the result carries one (`engineOptions.timestamp`). The same scan always renders byte-identical XML, so a report can be committed or diffed.
|
|
72
|
+
|
|
73
|
+
Text is escaped for XML, and characters XML 1.0 forbids even when escaped (most control characters, lone surrogates) are dropped, since markup captured from a page can contain them.
|
package/docs/LIMITATIONS.md
CHANGED
|
@@ -13,13 +13,16 @@ surea11y is a **static DOM scan**: it reads the DOM tree and computed styles at
|
|
|
13
13
|
## Environment-dependent — depends on how you run it
|
|
14
14
|
|
|
15
15
|
- **jsdom (Node, no real browser) has no CSS layout engine.** Rules needing real geometry — most notably `target-size-minimum` (WCAG 2.5.8, needs real `getBoundingClientRect()`) — report `notApplicable` under plain jsdom rather than guess. Run under a real browser (Puppeteer/Playwright — see [`INTEGRATION.md`](./INTEGRATION.md) Pattern 2) to get real findings from these rules.
|
|
16
|
+
- **Whether text wraps needs layout, so text-spacing findings on non-wrapping text are reported for review.** WCAG 1.4.12 and the ACT rules behind it apply only to text containing a soft wrap break, which a static scan cannot establish. `avoid-inline-spacing` treats text as wrapping by default, so an ordinary forced value below the metric still fails; where no wrap is possible for a reason that *is* visible without layout — text not allowed to wrap, or a fixed-width element inside a horizontally scrolling ancestor — it reports `cantTell` instead. Text that never wraps for some other reason is still reported as a failure.
|
|
16
17
|
- **`<dialog>` and other elements hidden by the default UA stylesheet** (no `open` attribute, `display: none` by spec), along with any other subtree hidden via `display:none`, `visibility:hidden`, `[hidden]`, or closed `<details>`, are **excluded from rule evaluation by default** — matching the visibility-aware behavior of other established engines. This is a deliberate default (`engineOptions.includeHiddenElements: false`), not an oversight: hidden content isn't reachable by assistive technology or keyboard until it's shown, so flagging a markup defect inside it by default would often be noise. Set `engineOptions.includeHiddenElements: true` to evaluate hidden/collapsed subtrees anyway — e.g. to catch a markup defect (like a broken ARIA ID reference) before a dialog ever opens. See [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md#engineoptions--the-rest) for the option and exactly which hiding mechanisms it covers.
|
|
17
18
|
- **jsdom's computed `text-shadow` is unreliable on a second read of the same element.** Confirmed in jsdom 29.1.1: reading a computed `text-shadow` value a second time on the same element — through any accessor, from any freshly-requested `CSSStyleDeclaration` for that element, regardless of caching — silently returns a different, wrong "no shadow" value instead of the real declared one. The first read is always correct. This engine works around it internally by reading each element's `text-shadow` exactly once per run and caching the result (see `__textShadowInfoEl` in `src/core/contrast-helpers.js`), so a single scan is unaffected. It only surfaces if you read `getComputedStyle(el).textShadow` yourself, more than once, against the same jsdom-parsed element — a real browser has no such bug.
|
|
19
|
+
- **Under jsdom, scan time grows with the square of DOM depth, not with element count.** jsdom resolves inherited CSS by walking an element's ancestor chain on every `getComputedStyle` call, so one call per element costs O(elements x depth). Measured on jsdom 29.1.1 with no engine code involved: 4000 elements in one chain take 8.3s of `getComputedStyle` alone, against 0.25s for the same 4000 as siblings. The engine's own ancestor walks are capped and stay linear, so this is jsdom's cost rather than the rules'. It affects `runDomRulesInPage` and anything built on it, including `@surea11y/test-matchers`; a real browser computes inherited style natively and does not have this shape. Component frameworks routinely nest 100-300 deep, which is comfortably fast — it becomes noticeable past roughly 1000.
|
|
20
|
+
- **In a real browser, some rules change the page for the time of their check, and the page's own scripts can react.** To measure what the browser draws, `css-hidden-focus` focuses elements as the keyboard would, and switches transitions off on the element it measures; `text-spacing-content-loss` adds a style sheet with the WCAG 1.4.12 spacing. All of this is put back before the rule returns: focus, scroll position, style attributes and the added style sheet. What cannot be put back is what the page's scripts do in response, since focusing an element fires its `focus` handlers: a carousel may move to the focused slide or enable a button. The findings are not affected, but if you scan a page you go on using, reload it afterwards.
|
|
18
21
|
- **Static markup vs. live/post-hydration DOM state.** The rule logic itself is DOM-source-agnostic — it evaluates whatever DOM it's handed, whether that's jsdom-parsed static HTML (Pattern 1) or an already-loaded, already-hydrated real browser tab (Pattern 2, see [`INTEGRATION.md`](./INTEGRATION.md)). But the CLI (`npx @surea11y/cli scan <url>`) specifically fetches static HTML only, with no JS execution — see [the CLI docs](https://github.com/SureA11y/cli/blob/main/docs/CLI.md). For a JS-framework-hydrated widget whose server-rendered markup intentionally ships one state before client JS syncs it (e.g. `<input type="checkbox" aria-checked="true">` shipped before client JS sets the native `checked` property to match on hydration — an extremely common, entirely legitimate pattern), a CLI scan only sees the pre-hydration markup. A scan running inside an actual loaded browser tab sees the post-hydration state instead, so the two can disagree on exactly this class of element for reasons that have nothing to do with rule correctness. That's why `aria-checked-state-mismatch` is capped at `manual`/`cantTell` rather than a hard `fail`. If you need live-DOM accuracy for hydration-sensitive checks, run the library directly against an already-loaded page via Pattern 2, not the static-fetch CLI.
|
|
19
22
|
|
|
20
23
|
## Not attempted: judgment calls that aren't automatable safely
|
|
21
24
|
|
|
22
|
-
These have no comparably safe heuristic at this engine's
|
|
25
|
+
These have no comparably safe heuristic at this engine's bar (`fail` must stay reserved for deterministic violations, full stop). Building them anyway would either catch almost nothing (too narrow to be useful) or risk real false positives (too broad to trust):
|
|
23
26
|
|
|
24
27
|
- **"Is this heading/label text meaningful?"** — real headings and labels are enormously varied and legitimately short ("FAQ," "Name," "Overview" are all fine), so nothing decides from markup whether a heading describes the section under it or a label describes the field beside it. What *is* decidable is that some strings cannot describe anything: `heading-quality` and `form-control-label-quality` flag leftover placeholders, numbered template slots, filenames and URLs against curated exact-match lists, the same precision-over-recall trade-off `link-name-quality` makes. Both are `manual` rules capped at `cantTell` — they raise a candidate for review, they never assert the text is wrong.
|
|
25
28
|
- **"Does this error message describe the problem?"** — what triggers a validation error and its content are almost always JS/validation-library-driven, invisible to a static scan in the first place; not just a heuristic-design problem.
|