@surea11y/core 1.4.0 → 1.5.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 +88 -7
- package/README.md +19 -3
- package/bin/surea11y-core.js +0 -0
- package/docs/API_STABILITY.md +2 -2
- package/docs/ARIA_DEPRECATION.md +95 -0
- package/docs/ENGINE_OPTIONS.md +14 -10
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +28 -6
- package/docs/LIMITATIONS.md +1 -1
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +2 -0
- package/docs/RULE_AUTHORING.md +53 -0
- package/docs/RULE_CATALOG.md +6 -6
- package/docs/TROUBLESHOOTING.md +2 -2
- package/package.json +8 -1
- package/src/checks/automatic/aria-allowed-attr.js +661 -99
- package/src/checks/automatic/aria-allowed-role.js +14 -16
- package/src/checks/automatic/aria-braille-equivalent.js +17 -19
- package/src/checks/automatic/aria-conditional-attr.js +17 -19
- package/src/checks/automatic/aria-deprecated-role.js +122 -41
- package/src/checks/automatic/aria-hidden-body.js +2 -9
- package/src/checks/automatic/aria-hidden-focus.js +99 -18
- package/src/checks/automatic/aria-prohibited-attr.js +54 -55
- package/src/checks/automatic/aria-prohibited-children.js +26 -26
- package/src/checks/automatic/aria-required-attr.js +14 -17
- package/src/checks/automatic/aria-required-children.js +17 -20
- package/src/checks/automatic/aria-required-parent.js +17 -20
- package/src/checks/automatic/aria-roles-valid.js +66 -27
- package/src/checks/automatic/aria-valid-attr-value.js +18 -21
- package/src/checks/automatic/aria-valid-attr.js +14 -17
- package/src/checks/automatic/autocomplete-valid.js +52 -18
- package/src/checks/automatic/avoid-inline-spacing.js +192 -34
- package/src/checks/automatic/binary-control-name-present.js +30 -24
- package/src/checks/automatic/button-name-present.js +73 -45
- package/src/checks/automatic/canvas-text-alternative-present.js +15 -8
- package/src/checks/automatic/combobox-name-present.js +23 -18
- package/src/checks/automatic/css-orientation-lock.js +22 -22
- package/src/checks/automatic/definition-list-children-valid.js +18 -21
- package/src/checks/automatic/deprecated-elements-not-used.js +14 -16
- package/src/checks/automatic/dialog-name-present.js +24 -19
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +45 -37
- package/src/checks/automatic/form-control-programmatic-label-present.js +48 -4
- package/src/checks/automatic/form-control-single-label.js +109 -43
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +29 -31
- package/src/checks/automatic/iframe-name-present.js +15 -17
- package/src/checks/automatic/iframe-title-unique.js +18 -23
- package/src/checks/automatic/img-alt-present.js +16 -9
- package/src/checks/automatic/input-image-alt-present.js +99 -48
- package/src/checks/automatic/label-in-name.js +102 -33
- package/src/checks/automatic/language-page-present.js +5 -1
- package/src/checks/automatic/link-in-text-block.js +19 -21
- package/src/checks/automatic/link-name-present.js +75 -47
- package/src/checks/automatic/list-children-valid.js +15 -17
- package/src/checks/automatic/listbox-name-present.js +23 -18
- package/src/checks/automatic/listitem-parent-valid.js +14 -17
- package/src/checks/automatic/menuitem-name-present.js +24 -19
- package/src/checks/automatic/meta-refresh-no-exceptions.js +44 -18
- package/src/checks/automatic/meta-refresh-timing-absent.js +44 -21
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +51 -32
- package/src/checks/automatic/meter-name-present.js +24 -19
- package/src/checks/automatic/nested-interactive-controls-absent.js +179 -51
- package/src/checks/automatic/object-text-alternative-present.js +14 -7
- package/src/checks/automatic/option-name-present.js +24 -19
- package/src/checks/automatic/progressbar-name-present.js +27 -22
- package/src/checks/automatic/searchbox-name-present.js +27 -18
- package/src/checks/automatic/server-side-image-map-absent.js +15 -18
- package/src/checks/automatic/slider-name-present.js +26 -19
- package/src/checks/automatic/spinbutton-name-present.js +27 -18
- package/src/checks/automatic/summary-name-present.js +24 -19
- package/src/checks/automatic/tab-name-present.js +24 -19
- package/src/checks/automatic/table-headers-attr-valid.js +15 -17
- package/src/checks/automatic/table-th-has-data-cells.js +82 -25
- package/src/checks/automatic/target-size-minimum.js +104 -81
- package/src/checks/automatic/td-has-header.js +15 -20
- package/src/checks/automatic/textbox-name-present.js +23 -18
- package/src/checks/automatic/tooltip-name-present.js +24 -19
- package/src/checks/automatic/treeitem-name-present.js +24 -19
- package/src/checks/automatic/valid-lang.js +31 -20
- package/src/checks/manual/accesskeys-manual.js +18 -19
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +19 -22
- package/src/checks/{automatic/bypass-blocks-present.js → manual/bypass-blocks-present-manual.js} +98 -44
- package/src/checks/manual/empty-heading-manual.js +15 -17
- package/src/checks/manual/empty-table-header-manual.js +27 -30
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -9
- package/src/checks/manual/heading-order-manual.js +17 -22
- package/src/checks/manual/image-redundant-alt-manual.js +14 -17
- package/src/checks/manual/input-image-alt-decorative-manual.js +24 -0
- package/src/checks/manual/label-title-only-manual.js +15 -17
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +25 -39
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +21 -27
- package/src/checks/manual/landmark-main-is-top-level-manual.js +14 -17
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +2 -7
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -7
- package/src/checks/manual/landmark-one-main-manual.js +2 -9
- package/src/checks/manual/landmark-unique-manual.js +22 -27
- package/src/checks/manual/link-name-quality-manual.js +15 -17
- package/src/checks/manual/meta-viewport-large-manual.js +14 -17
- package/src/checks/manual/mouse-only-event-handlers-manual.js +17 -19
- package/src/checks/manual/page-has-heading-one-manual.js +2 -9
- package/src/checks/manual/presentation-role-conflict-manual.js +19 -21
- package/src/checks/manual/region-manual.js +13 -6
- package/src/checks/manual/scope-attr-valid-manual.js +14 -17
- package/src/checks/manual/skip-link-manual.js +39 -47
- package/src/checks/manual/tabindex-manual.js +14 -17
- package/src/checks/manual/table-duplicate-name-manual.js +14 -17
- package/src/core.js +8818 -4330
- package/src/report.js +14 -0
- package/src/sarif.js +2 -2
- package/surea11y.browser.js +4023 -3911
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
package/docs/I18N.md
CHANGED
|
@@ -4,14 +4,18 @@ Every rule's title/description, and every occurrence's summary/hint, is localize
|
|
|
4
4
|
|
|
5
5
|
## Current locale coverage
|
|
6
6
|
|
|
7
|
-
| Locale | File | Keys |
|
|
7
|
+
| Locale | File | Keys | Values still in English |
|
|
8
8
|
|---|---|---|---|
|
|
9
|
-
| `en` (English) | `src/i18n/en.
|
|
10
|
-
| `fr` (French) | `src/i18n/fr.
|
|
11
|
-
| `de` (German) | `src/i18n/de.
|
|
12
|
-
| `es` (Spanish) | `src/i18n/es.
|
|
9
|
+
| `en` (English) | `src/i18n/en.json` | 633 | — (the canonical/fallback set) |
|
|
10
|
+
| `fr` (French) | `src/i18n/fr.json` | 633 | 1 |
|
|
11
|
+
| `de` (German) | `src/i18n/de.json` | 633 | 0 |
|
|
12
|
+
| `es` (Spanish) | `src/i18n/es.json` | 633 | 0 |
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
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
|
+
|
|
16
|
+
Every locale carries every key `en.json` 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
|
+
|
|
18
|
+
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.
|
|
15
19
|
|
|
16
20
|
## Selecting a locale
|
|
17
21
|
|
|
@@ -19,17 +23,87 @@ All four locales are fully translated as of this writing. That won't stay automa
|
|
|
19
23
|
runDomRulesInPage(url, null, { locale: 'fr' }, null);
|
|
20
24
|
```
|
|
21
25
|
|
|
22
|
-
Default is `'en'` if omitted. Any string is accepted
|
|
26
|
+
Default is `'en'` if omitted. Any string is accepted; an unrecognized locale is never an error.
|
|
27
|
+
|
|
28
|
+
## How a locale is chosen
|
|
29
|
+
|
|
30
|
+
Two things happen, in this order. Getting them mixed up is the usual source of confusion, so they are described separately.
|
|
31
|
+
|
|
32
|
+
### Step 1 — pick a dictionary (once per scan)
|
|
33
|
+
|
|
34
|
+
1. Use the dictionary matching your code. `de` finds `de.json`. Case doesn't matter — `pt-br`, `pt-BR` and `PT-BR` all find `pt-BR.json`.
|
|
35
|
+
2. Otherwise, drop everything after the first `-` and try that. `de-DE` finds `de.json`; so does `de-AT`.
|
|
36
|
+
3. Otherwise, use English.
|
|
37
|
+
|
|
38
|
+
So you only need a `de-DE.json` if German in Austria and Germany should actually read differently. Ship `de.json` and every German variant is covered.
|
|
39
|
+
|
|
40
|
+
### Step 2 — resolve each string (per string)
|
|
41
|
+
|
|
42
|
+
Within the chosen dictionary, every string is looked up on its own:
|
|
43
|
+
|
|
44
|
+
1. Take the key's value from the chosen dictionary.
|
|
45
|
+
2. If that key is missing, take the English one.
|
|
46
|
+
3. If English is missing it too — which shouldn't happen for a built-in key, but can for a hand-rolled one — use the literal text the rule itself carries.
|
|
47
|
+
|
|
48
|
+
The result is never a blank, an `undefined`, or a thrown error. A half-finished translation renders in your language where it exists and in English everywhere else. See `t()` in `scripts/build-core.js` for the exact implementation.
|
|
49
|
+
|
|
50
|
+
## Knowing which locale you actually got
|
|
51
|
+
|
|
52
|
+
Graceful fallback has one drawback: ask for a language the build doesn't carry and you get fluent English back, with nothing in the strings to say so. Every result therefore reports the resolution once, at the top:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
"engine": {
|
|
56
|
+
"tag": "a11ycore",
|
|
57
|
+
"schemaVersion": "1.0.0",
|
|
58
|
+
"locale": { "requested": "ja", "resolved": "en", "reason": "unknown-locale" }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`requested` is what you asked for (after trimming; `en` if you passed nothing or a non-string), `resolved` is the dictionary that was used, and `reason` is one of:
|
|
63
|
+
|
|
64
|
+
| `reason` | Meaning |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `ok` | You got exactly what you asked for, and that dictionary carries every key. |
|
|
67
|
+
| `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
|
+
| `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. `ja` and `pt-BR` both land here today. |
|
|
70
|
+
| `partial-dictionary` | The dictionary was used but is missing some keys, so those individual strings fell back to English. |
|
|
23
71
|
|
|
24
|
-
|
|
72
|
+
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.
|
|
25
73
|
|
|
26
|
-
|
|
74
|
+
If you need a particular language, `requested !== resolved` is the condition to check in CI. Note it is also true for the harmless `primary-subtag` case, so gate on `reason === 'unknown-locale' || reason === 'dictionary-not-loaded'` if a base-language match is good enough for you. See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md#top-level-result) for the field's place in the result and [`API_STABILITY.md`](./API_STABILITY.md) for what is guaranteed about it.
|
|
27
75
|
|
|
28
|
-
|
|
29
|
-
2. If missing, fall back to the English (`en`) dictionary.
|
|
30
|
-
3. If still missing (shouldn't happen for a built-in key, but true for anything hand-rolled), fall back to the literal string the rule itself provided as a default.
|
|
76
|
+
## Where the dictionaries live
|
|
31
77
|
|
|
32
|
-
|
|
78
|
+
Which languages are available depends on how you load the engine.
|
|
79
|
+
|
|
80
|
+
| How you load it | What you get |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `require('@surea11y/core')` | Every locale, built in. Nothing to configure. |
|
|
83
|
+
| A binding (Playwright, Cypress, …) | Every locale, built in. Nothing to configure. |
|
|
84
|
+
| `surea11y.browser.js` in a `<script>` tag | English. Load `surea11y.i18n.<locale>.js` after it for anything else. |
|
|
85
|
+
|
|
86
|
+
The bundle is split because it travels over the network to every page that uses it, and no page needs all four languages. 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
|
+
|
|
88
|
+
```html
|
|
89
|
+
<script src="surea11y.browser.js"></script>
|
|
90
|
+
<script src="surea11y.i18n.de.js"></script>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
### Supplying a dictionary yourself
|
|
96
|
+
|
|
97
|
+
`engineOptions.messages` accepts `{ [locale]: { key: value } }` and takes precedence over anything built in or loaded from a side file. Useful for overriding a handful of strings, or for a language you maintain privately:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
runDomRulesInPage(url, null, {
|
|
101
|
+
locale: 'de',
|
|
102
|
+
messages: { de: { img_altPresent_title: 'Eigener Text' } }
|
|
103
|
+
}, null);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Keys you don't supply fall back normally, so a partial override is fine.
|
|
33
107
|
|
|
34
108
|
## Where keys are used
|
|
35
109
|
|
|
@@ -40,11 +114,93 @@ Two independent key namespaces, both resolved the same way:
|
|
|
40
114
|
|
|
41
115
|
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`.
|
|
42
116
|
|
|
43
|
-
##
|
|
117
|
+
## Adding your language
|
|
118
|
+
|
|
119
|
+
You do not need to know how the engine works, and you will not write any JavaScript beyond editing quoted strings. A translation is one data file.
|
|
120
|
+
|
|
121
|
+
### 1. Get the repository running
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
git clone https://github.com/SureA11y/core.git
|
|
125
|
+
cd core
|
|
126
|
+
npm install
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 2. Create the file
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
npm run i18n:new pt-BR
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
That writes `src/i18n/pt-BR.json` containing every key `en.json` has, in the same order, each 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
|
+
|
|
137
|
+
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
|
+
|
|
139
|
+
The command refuses to overwrite a file that already exists. To pick up work on an existing locale, edit it directly.
|
|
140
|
+
|
|
141
|
+
### 3. Translate the values
|
|
142
|
+
|
|
143
|
+
Each entry is `"key": "text"`. Change only the text on the right:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
"img_altPresent_title": "<img> must have an alt attribute",
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
becomes
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
"img_altPresent_title": "<img> precisa ter um atributo alt",
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Four things to leave alone:
|
|
156
|
+
|
|
157
|
+
- **The keys.** `img_altPresent_title` is an identifier the engine looks up. Renaming one breaks the lookup.
|
|
158
|
+
- **`{{placeholder}}` tokens** — `{{ratio}}`, `{{element}}`, `{{role}}` and friends are substituted with real values at scan time. Keep them spelled exactly as in English. You may move them within the sentence if your grammar needs it.
|
|
159
|
+
- **`{{#name}}…{{/name}}` and `{{^name}}…{{/name}}` blocks** — conditional sections, shown or hidden depending on the finding. Translate the text inside them; keep the markers.
|
|
160
|
+
- **Code identifiers** — `<img>`, `aria-label`, `role="dialog"`, `alt=""`, CSS property names. These are things the reader will look for in their own source, so they stay in the original. A double quote inside a value has to stay escaped as `\"`, which is the one piece of JSON syntax you need.
|
|
161
|
+
|
|
162
|
+
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
|
+
|
|
164
|
+
If a string is genuinely identical in your language, leave it. It is counted as untranslated but behaves correctly.
|
|
165
|
+
|
|
166
|
+
### 4. Check your progress
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
npm run i18n:report
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Prints, per locale, how many values differ from English, plus any missing or orphaned keys. Anything still matching the English text is reported as untranslated — that is a progress signal, not an error.
|
|
173
|
+
|
|
174
|
+
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
|
+
|
|
176
|
+
### 5. Before opening the pull request
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
npm run i18n:sync # confirms your file matches en.json key-for-key
|
|
180
|
+
npm test
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`npm run build` also emits `surea11y.i18n.<locale>.js` for the standalone browser bundle — generated, so there is nothing for you to write.
|
|
184
|
+
|
|
185
|
+
Then open a pull request touching `src/i18n/<locale>.json`, 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
|
+
|
|
187
|
+
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
|
+
|
|
189
|
+
## Maintaining the locales
|
|
190
|
+
|
|
191
|
+
When you add, rename or remove a key in `src/i18n/en.json`:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
npm run i18n:sync
|
|
195
|
+
```
|
|
196
|
+
|
|
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 locale.
|
|
198
|
+
|
|
199
|
+
| Command | Does |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `npm run i18n:new <locale>` | Create a new locale file from `en.json`. Refuses to overwrite. |
|
|
202
|
+
| `npm run i18n:sync` | Bring every locale file back in line with `en.json`. Add `-- <locale>` to restrict it to one. |
|
|
203
|
+
| `npm run i18n:check` | Same comparison, writes nothing, exits non-zero on drift. |
|
|
204
|
+
| `npm run i18n:report` | Per-locale translation coverage. |
|
|
44
205
|
|
|
45
|
-
|
|
46
|
-
2. Replace the placeholder values with real translations, key by key. Leave any you're unsure about as-is for now — a value identical to English is treated as untranslated, not broken (see the fallback behavior above).
|
|
47
|
-
3. Check progress any time with `npm run i18n:report` — it prints, per locale, how many keys have been translated vs. still match the English placeholder, plus any missing or orphaned keys.
|
|
48
|
-
4. You don't need every key on day one — the fallback behavior above means a partial translation degrades gracefully to English per-string, exactly like `fr`/`de`/`es` did during their own early stages. Ship what you have. A partial locale is valid and won't fail `i18n-locale-completeness.test.js` unless you also add it to `FULLY_TRANSLATED_LOCALES` in that file — only do that once `npm run i18n:report` shows 100% coverage.
|
|
49
|
-
5. Keep `{{placeholder}}` tokens (and `{{#foo}}...{{/foo}}` conditional blocks) in translated strings exactly as they appear in the English source — they're substituted/evaluated verbatim regardless of locale (e.g. `{{element}}`, `{{role}}`). Never translate HTML tag/attribute names (`<img>`, `aria-label`, `role="dialog"`, etc.) — they're code identifiers, not prose.
|
|
50
|
-
6. Run `npm run build && npm test` to confirm nothing broke, including locale-completeness.
|
|
206
|
+
`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
|
@@ -107,7 +107,29 @@ Good for: a manual check against a page open in a real browser, a bookmarklet, o
|
|
|
107
107
|
|
|
108
108
|
`a11ycore.runa11yCoreInPage` is the exact same function described in Pattern 2 above — the bundle exists only to solve *loading* it without `require`/a module system, not to add a separate API surface. It carries the same self-containment property (own inlined rule catalog, no free variables), which is what makes a plain `<script>` tag sufficient.
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
### Scanning in a language other than English
|
|
111
|
+
|
|
112
|
+
The bundle carries English only. Every other locale ships beside it as its own file — load one after the bundle and that language becomes available:
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
<script src="node_modules/@surea11y/core/surea11y.browser.js"></script>
|
|
116
|
+
<script src="node_modules/@surea11y/core/surea11y.i18n.de.js"></script>
|
|
117
|
+
<script>
|
|
118
|
+
const result = a11ycore.runa11yCoreInPage(location.href, null, { locale: 'de' }, null);
|
|
119
|
+
</script>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Load as many as you need; each adds one language and they don't interfere. Order matters only in that the bundle has to come first — a side file loaded on its own throws with a message saying so.
|
|
123
|
+
|
|
124
|
+
Ask for a locale whose file you haven't loaded and you get English, not an error, with `result.engine.locale.reason` set to `dictionary-not-loaded` so it's visible rather than silent. See [`I18N.md`](./I18N.md).
|
|
125
|
+
|
|
126
|
+
Why the split: the bundle is fetched over the network, and every language would otherwise be paid for by every page whether used or not. Keeping English inline and the rest optional took roughly 280 KB off the download and stops it growing as languages are added. The Node package is unaffected — `require('@surea11y/core')` still has every locale built in.
|
|
127
|
+
|
|
128
|
+
If you'd rather supply a dictionary yourself, `engineOptions.messages` takes `{ [locale]: { key: value } }` directly and wins over a loaded side file.
|
|
129
|
+
|
|
130
|
+
### What the bundle leaves out
|
|
131
|
+
|
|
132
|
+
Deliberately excluded: `runa11yCoreAcrossFrames`/`a11yCoreEnableFrameResponder`. Cross-frame scanning needs the embedded frame to load the engine and opt in too (see "Cross-frame scanning" below) — not a fit for a single dropped-in script tag. Use the npm package directly if you need it.
|
|
111
133
|
|
|
112
134
|
## Scoping a scan to part of the page
|
|
113
135
|
|
|
@@ -139,11 +161,11 @@ Notes for CI specifically:
|
|
|
139
161
|
|
|
140
162
|
### Cross-frame scanning (including cross-origin)
|
|
141
163
|
|
|
142
|
-
`runa11yCoreInPage` only ever scans the single document it runs in — it has no visibility into `<iframe>` content, same-origin or not. For most uses that's fine (rules apply to the current document; a consumer running once per frame, e.g. once per content-script injection into `all_frames: true`, already covers every frame independently). But sometimes you want ONE scan's result to include what's inside embedded frames too — payment widgets, cookie-consent dialogs, third-party embeds —
|
|
164
|
+
`runa11yCoreInPage` only ever scans the single document it runs in — it has no visibility into `<iframe>` content, same-origin or not. For most uses that's fine (rules apply to the current document; a consumer running once per frame, e.g. once per content-script injection into `all_frames: true`, already covers every frame independently). But sometimes you want ONE scan's result to include what's inside embedded frames too — payment widgets, cookie-consent dialogs, third-party embeds — which is what the cross-frame functions below are for.
|
|
143
165
|
|
|
144
|
-
`runa11yCoreAcrossFrames`/`a11yCoreEnableFrameResponder` are a separate, additive pair of functions for exactly this — not needed at all if you're driving the browser with Puppeteer/Playwright (see "Pattern 2" above): an automation driver already reaches every frame unconditionally via CDP, which is strictly *better* than what's described here. This exists specifically for when there's **no automation driver** — a plain script/bundled widget/browser extension running inside the page itself, fully subject to the same-origin policy
|
|
166
|
+
`runa11yCoreAcrossFrames`/`a11yCoreEnableFrameResponder` are a separate, additive pair of functions for exactly this — not needed at all if you're driving the browser with Puppeteer/Playwright (see "Pattern 2" above): an automation driver already reaches every frame unconditionally via CDP, which is strictly *better* than what's described here. This exists specifically for when there's **no automation driver** — a plain script/bundled widget/browser extension running inside the page itself, fully subject to the same-origin policy.
|
|
145
167
|
|
|
146
|
-
**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** —
|
|
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.
|
|
147
169
|
|
|
148
170
|
```js
|
|
149
171
|
// Inside the embedded/child page (e.g. a widget's own bundle), once, at load:
|
|
@@ -167,8 +189,8 @@ for (const frame of result.frames) {
|
|
|
167
189
|
|
|
168
190
|
A few things worth knowing:
|
|
169
191
|
- **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).
|
|
170
|
-
- **`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
|
|
192
|
+
- **`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.
|
|
171
193
|
- **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.
|
|
172
194
|
- **Bundler-free, like `runa11yCoreInPage`** — both functions are fully self-contained (their own private copy of the rule catalog and every helper they need), so raw-source injection (a bookmarklet, a content script with no build step) works with zero bundler needed, exactly like `runa11yCoreInPage` already does. If you *do* use a normal bundler/`require`/`import`, that works too, unchanged.
|
|
173
195
|
- **Cost of that self-containment**: `src/core.js` grew from ~1.86MB to ~3.1MB, since these two functions each needed their own complete private copy of the rule catalog and shared helpers rather than sharing the outer `RULE_IMPLS`. If this file's size ever becomes a real problem, the fix would be to drop the bundler-free requirement for just these two functions (accepting that cross-frame scanning in "plain script injection" mode needs a real bundler, unlike `runa11yCoreInPage` alone) rather than tripling the embedded catalog again for some future feature.
|
|
174
|
-
- **No origin/identity check on the sender** beyond the message's own namespaced envelope
|
|
196
|
+
- **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/LIMITATIONS.md
CHANGED
|
@@ -14,7 +14,7 @@ surea11y is a **static DOM scan**: it reads the DOM tree and computed styles at
|
|
|
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
16
|
- **`<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
|
-
- **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.
|
|
17
|
+
- **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. `aria-checked-state-mismatch` is deliberately `manual`/`cantTell`-capped for this exact reason 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.
|
|
18
18
|
|
|
19
19
|
## Deliberately not attempted — judgment calls, not automatable safely
|
|
20
20
|
|
package/docs/OUTPUT_SCHEMA.md
CHANGED
|
@@ -15,7 +15,11 @@ This is the exact shape of the object returned by `runDomRulesInPage(...)` / `ru
|
|
|
15
15
|
|
|
16
16
|
```ts
|
|
17
17
|
{
|
|
18
|
-
engine: {
|
|
18
|
+
engine: {
|
|
19
|
+
tag: string,
|
|
20
|
+
schemaVersion: string,
|
|
21
|
+
locale: { requested: string, resolved: string, reason: string }
|
|
22
|
+
},
|
|
19
23
|
url: string | null,
|
|
20
24
|
title: string | null,
|
|
21
25
|
timestamp: string | null,
|
|
@@ -31,6 +35,8 @@ This is the exact shape of the object returned by `runDomRulesInPage(...)` / `ru
|
|
|
31
35
|
|---|---|
|
|
32
36
|
| `engine.tag` | The engine's own identity tag, currently `"a11ycore"`. Every rule (built-in or custom) carries it in `meta.tags` — rule `ruleId`s themselves are bare (no prefix). |
|
|
33
37
|
| `engine.schemaVersion` | The result-schema version (`"1.0.0"`). Bump-worthy if this document's shape ever changes incompatibly — pin to it if you're parsing output programmatically. See [`API_STABILITY.md`](./API_STABILITY.md) for the full stable/unstable field list and version-bump policy. |
|
|
38
|
+
| `engine.locale` | Which dictionary the run actually used. `requested` is your `engineOptions.locale` after trimming (`"en"` if you passed nothing or a non-string); `resolved` is the locale whose dictionary was used; `reason` explains the pairing. Because locale fallback is graceful and per-string, asking for a language the build does not carry produces English text rather than an error — this field is how you find that out without reading the strings. Reported once per result: a run uses one dictionary throughout. |
|
|
39
|
+
| `engine.locale.reason` | `"ok"` — you got the dictionary you asked for, and it carries every key. `"primary-subtag"` — your code had a subtag with no dictionary of its own, so its base language was used: `"de-DE"` resolves to `"de"`. `"dictionary-not-loaded"` — the project ships that language, but this build does not carry it and none was supplied (the standalone browser bundle, without its locale side file). `"unknown-locale"` — the project has no such translation at all. `"partial-dictionary"` — the dictionary was used but is missing keys English has, so those strings fell back to English. Treat the set as open; later releases can add to it. |
|
|
34
40
|
| `url` | The `pageUrl` argument you passed in, or `document.location.href` if you passed `null`/omitted it, or `null` if neither is available. |
|
|
35
41
|
| `title` | `document.title` at scan time, or `null`. |
|
|
36
42
|
| `timestamp` | **Not auto-generated.** Only set if you pass `engineOptions.timestamp` as a non-empty string — the engine has no built-in clock (deterministic-by-design). If you want a scan timestamp in the result, supply it yourself. |
|
|
@@ -97,7 +103,7 @@ Notes:
|
|
|
97
103
|
|
|
98
104
|
- **`outcome` vs `outcomeNormalized`**: identical except `notApplicable` becomes `"inapplicable"` in `outcomeNormalized`. Both are provided so you can match either your own vocabulary or the engine's internal one.
|
|
99
105
|
- **`type: "manual"` rules can never report `outcome: "fail"`.** If a manual rule's own logic would have said `fail`, the engine coerces it to `cantTell` and appends an explanatory note to `error` — this is enforced centrally (`policy.coerceManualFailToCantTell`, on by default under the `a11y` policy contract; see [`POLICY.md`](./POLICY.md)), not something each rule has to remember. `fail` is reserved for deterministic, high-confidence, `type: "automatic"` findings only.
|
|
100
|
-
- **`meta.normativeMappings`** is how a check result ties back to a WCAG Success Criterion — `[]` for rules with no formal WCAG mapping (
|
|
106
|
+
- **`meta.normativeMappings`** is how a check result ties back to a WCAG Success Criterion — `[]` for rules with no formal WCAG mapping (this engine calls them advisory `type: "manual"` rules). See [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for how these roll up.
|
|
101
107
|
- **`error`**: only present if the rule implementation threw an uncaught exception, or if the manual-fail coercion above fired. A thrown rule always surfaces as `outcome: "cantTell"` with `occurrences: []` and `error` set to the exception message — the engine never lets one broken rule crash the whole scan.
|
|
102
108
|
- **`engineOptions`** on each result is the *resolved* options object (after locale/contrast defaults were applied), not literally what you passed in — useful for confirming what a given rule actually saw, especially the resolved `locale` and `contrast.mode`/`contrast.rootCanvasFallback`.
|
|
103
109
|
|
|
@@ -185,7 +191,11 @@ const result = runDomRulesInPage(
|
|
|
185
191
|
|
|
186
192
|
```json
|
|
187
193
|
{
|
|
188
|
-
"engine": {
|
|
194
|
+
"engine": {
|
|
195
|
+
"tag": "a11ycore",
|
|
196
|
+
"schemaVersion": "1.0.0",
|
|
197
|
+
"locale": { "requested": "en", "resolved": "en", "reason": "ok" }
|
|
198
|
+
},
|
|
189
199
|
"url": "https://example.test/",
|
|
190
200
|
"title": "Example",
|
|
191
201
|
"timestamp": null,
|
package/docs/REPORT.md
CHANGED
|
@@ -15,6 +15,8 @@ Open `report.html` directly from disk. Works alongside any other output mode —
|
|
|
15
15
|
- **WCAG rollup**: grouped by conformance level (A / AA / AAA), sourced directly from the engine's own `rulesResults[]` composite rollups (`docs/WCAG_CONFORMANCE.md`) — one row per Success Criterion, its outcome, a pass/fail/needs-review/n/a breakdown, and which atomic rules contributed. This is real engine data, not an invented grouping — the same rollup you'd get from the raw JSON's `rulesResults`.
|
|
16
16
|
- **Full technical data** (collapsed by default): a scorecard (tiles per outcome) and a searchable, filterable (by outcome), paginated table of every individual occurrence across the whole scan.
|
|
17
17
|
|
|
18
|
+
The meta bar under the header carries the rule count, occurrence count, engine tag, schema version, and the locale the scan resolved to. Locale fallback is per-string and invisible in the text itself, so a report requested in a language the engine does not carry reads as an ordinary English one — the chip names the requested locale alongside the resolved one when the two differ. See [`I18N.md`](./I18N.md).
|
|
19
|
+
|
|
18
20
|
## Library usage
|
|
19
21
|
|
|
20
22
|
```js
|
package/docs/RULE_AUTHORING.md
CHANGED
|
@@ -132,6 +132,10 @@ The build/runtime resolves i18n by:
|
|
|
132
132
|
2) falling back to `en` if missing,
|
|
133
133
|
3) falling back to the literal `title`/`description` strings if still missing.
|
|
134
134
|
|
|
135
|
+
Add the key and its English text to `src/i18n/en.json`, then run
|
|
136
|
+
`npm run i18n:sync` so every other locale picks it up. `npm test` fails if you
|
|
137
|
+
forget. See [`I18N.md`](./I18N.md).
|
|
138
|
+
|
|
135
139
|
#### `meta.tags`
|
|
136
140
|
Tags are used for grouping/filtering. Typical tag families in this ruleset include:
|
|
137
141
|
- WCAG tagging: `wcag2a`, `wcag111`
|
|
@@ -163,6 +167,32 @@ const meta = {
|
|
|
163
167
|
|
|
164
168
|
---
|
|
165
169
|
|
|
170
|
+
## 4.3 Reporting an occurrence
|
|
171
|
+
|
|
172
|
+
Build occurrences with `helpers.reportOccurrence(element, { summary, hint, i18n, data })`
|
|
173
|
+
rather than assembling the object by hand:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
occurrences.push(helpers.reportOccurrence(el, { summary: '…', hint: '…' }));
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
It attaches the element for the engine to finalize, which is how `selector`,
|
|
180
|
+
`html` and `structuralPath` get filled in centrally instead of in each of the
|
|
181
|
+
124 rules.
|
|
182
|
+
|
|
183
|
+
**This is a performance contract, not just a convenience.** Every occurrence
|
|
184
|
+
gets a `structuralPath`. Given the element, the engine computes it directly.
|
|
185
|
+
Given only a hand-built occurrence, it re-finds the element with
|
|
186
|
+
`document.querySelector(selector)` — one DOM query per occurrence. That is
|
|
187
|
+
fine for a rule reporting a single document-level finding, and quadratic for
|
|
188
|
+
one reporting many: `region` hand-built its occurrences and took four minutes
|
|
189
|
+
on a thousand-element page, against under a second afterwards.
|
|
190
|
+
|
|
191
|
+
`perfStats.counters['structuralPath.selectorFallback']` counts how often the
|
|
192
|
+
engine had to re-find an element, so a slow rule can be spotted without
|
|
193
|
+
guessing. `tests/structural-path-fallback.test.js` fails if that count starts
|
|
194
|
+
growing with page size.
|
|
195
|
+
|
|
166
196
|
## 5) i18n in occurrences (repo reality)
|
|
167
197
|
|
|
168
198
|
Occurrences also support i18n via keys + params.
|
|
@@ -189,6 +219,29 @@ At normalization time, the engine:
|
|
|
189
219
|
Translation strings use `{{paramName}}` placeholders.
|
|
190
220
|
`params` is shallow-copied and passed into interpolation.
|
|
191
221
|
|
|
222
|
+
**A param carries a value, never prose.** Element names, roles, attribute names,
|
|
223
|
+
selectors, ids, counts and ratios are values: they read the same in every
|
|
224
|
+
language, because the author will search their own source for them. An English
|
|
225
|
+
word or sentence is not, and passing one means it stays English in every locale
|
|
226
|
+
— with nothing to reveal it, since the key is present everywhere and coverage
|
|
227
|
+
reports look complete.
|
|
228
|
+
|
|
229
|
+
When a message varies by case, give each case its own key rather than
|
|
230
|
+
interpolating the differing text:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
// wrong: the sentence lives in the rule, so no locale can reach it
|
|
234
|
+
i18n: { hintKey: 'myRule_hint_fail', params: { advice: 'Replace it with role="list".' } }
|
|
235
|
+
|
|
236
|
+
// right: one key per case, each translatable on its own
|
|
237
|
+
i18n: { hintKey: 'myRule_hint_fail_directory', params: { role } }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`tests/i18n/i18n-translatable-strings.test.js` fails any dictionary value with
|
|
241
|
+
no translatable text of its own, which catches the `"{{advice}}"` shape above.
|
|
242
|
+
It cannot catch a param carrying prose into an otherwise-normal sentence, so
|
|
243
|
+
that one is on you.
|
|
244
|
+
|
|
192
245
|
---
|
|
193
246
|
|
|
194
247
|
## 6) Helpers contract used by rules (ctx.helpers)
|
package/docs/RULE_CATALOG.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Generated from the compiled engine's own catalog (`getChecksCatalog()`/`getRulesCatalog()`) — run `node scripts/generate-rule-catalog.js` after `npm run build` to regenerate this file whenever rules change. Do not hand-edit.
|
|
4
4
|
|
|
5
|
-
**125 rules total:
|
|
5
|
+
**125 rules total: 76 automatic (WCAG-normative, can return `fail`), 49 manual (advisory/judgment-required, capped at `cantTell`). 101 carry at least one formal WCAG Success Criterion mapping.**
|
|
6
6
|
|
|
7
7
|
See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`severity` mean on a scan result, and [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for how these roll up to an SC-level conformance claim. For WCAG-facet-level coverage-gap tracking (which parts of an SC are and aren't automatable yet), see `coverage/coverage-report.md` instead — that one is organized by facet, this one by rule.
|
|
8
8
|
|
|
9
|
-
## Automatic rules (
|
|
9
|
+
## Automatic rules (76) — can return `fail`
|
|
10
10
|
|
|
11
11
|
| Rule ID | Title | WCAG SC | Level | Confidence | Default severity |
|
|
12
12
|
|---|---|---|---|---|---|
|
|
@@ -15,7 +15,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
|
|
|
15
15
|
| `aria-allowed-role` | Explicit role must be permitted for its host element | 4.1.2 | A | high | moderate |
|
|
16
16
|
| `aria-braille-equivalent` | aria-braillelabel/aria-brailleroledescription must have a non-braille equivalent | 4.1.2 | A | high | serious |
|
|
17
17
|
| `aria-conditional-attr` | aria-errormessage requires aria-invalid to be set to a non-false value | 4.1.2 | A | high | serious |
|
|
18
|
-
| `aria-deprecated-role` | role attribute
|
|
18
|
+
| `aria-deprecated-role` | role attribute should not use a deprecated or author-discouraged ARIA role | 4.1.2 | A | high | moderate |
|
|
19
19
|
| `aria-hidden-body` | The document <body> must not be aria-hidden | 1.3.1, 4.1.2 | A | high | critical |
|
|
20
20
|
| `aria-hidden-focus` | ARIA hidden elements must not be focusable | 2.4.7, 4.1.2 | AA | high | serious |
|
|
21
21
|
| `aria-prohibited-attr` | ARIA naming attributes must not be used on roles that prohibit them | 4.1.2 | A | high | moderate |
|
|
@@ -28,10 +28,9 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
|
|
|
28
28
|
| `aria-valid-attr` | aria-* attributes must be real, defined ARIA attributes | 4.1.2 | A | high | serious |
|
|
29
29
|
| `aria-valid-attr-value` | aria-* attribute values must match their declared type | 4.1.2 | A | high | serious |
|
|
30
30
|
| `autocomplete-valid` | autocomplete attribute must be a valid autofill value | 1.3.5 | AA | high | moderate |
|
|
31
|
-
| `avoid-inline-spacing` | Inline style must not force text spacing
|
|
31
|
+
| `avoid-inline-spacing` | Inline style must not force text spacing below the WCAG metric | 1.4.12 | AA | high | moderate |
|
|
32
32
|
| `binary-control-name-present` | Binary controls have an accessible name | 4.1.2 | A | high | serious |
|
|
33
33
|
| `button-name-present` | Buttons have an accessible name | 4.1.2 | A | high | serious |
|
|
34
|
-
| `bypass-blocks-present` | Page must provide a way to bypass repeated blocks | 2.4.1 | A | medium | serious |
|
|
35
34
|
| `canvas-text-alternative-present` | <canvas> must provide a text alternative | 1.1.1 | A | high | serious |
|
|
36
35
|
| `combobox-name-present` | Comboboxes have an accessible name | 4.1.2 | A | high | serious |
|
|
37
36
|
| `contrast-computable` | Color contrast is computable for rendered text | 1.4.3, 1.4.6 | AAA | high | serious |
|
|
@@ -88,7 +87,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
|
|
|
88
87
|
| `valid-lang` | Element lang attribute must be syntactically valid | 3.1.2 | AA | high | moderate |
|
|
89
88
|
| `video-poster-text-alternative-present` | <video> poster must have a text alternative | 1.1.1 | A | medium | serious |
|
|
90
89
|
|
|
91
|
-
## Manual rules (
|
|
90
|
+
## Manual rules (49) — advisory, capped at `cantTell`
|
|
92
91
|
|
|
93
92
|
| Rule ID | Title | WCAG SC | Level | Confidence | Default severity |
|
|
94
93
|
|---|---|---|---|---|---|
|
|
@@ -97,6 +96,7 @@ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`sever
|
|
|
97
96
|
| `area-alt-quality` | <area> alt text must be appropriate (manual review) | 1.1.1 | A | medium | minor |
|
|
98
97
|
| `aria-checked-state-mismatch` | Native checkbox/radio aria-checked should match its actual state | 4.1.2 | A | medium | moderate |
|
|
99
98
|
| `aria-text` | role="text" elements should have no focusable descendants | — | — | medium | minor |
|
|
99
|
+
| `bypass-blocks-present` | Page must provide a way to bypass repeated blocks | 2.4.1 | A | medium | moderate |
|
|
100
100
|
| `canvas-text-alternative-quality` | <canvas> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
|
|
101
101
|
| `css-hidden-focus` | Focusable elements must not be visually hidden | 2.4.7 | AA | low | serious |
|
|
102
102
|
| `embed-text-alternative-quality` | <embed> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## "I passed `runOnly: ['some-rule-id']` but every rule still ran"
|
|
4
4
|
|
|
5
|
-
`runOnly` must be an object, not a bare array — `runOnly: ['img-alt-present']` is silently ignored (the engine falls through to "run everything"), because that shape has none of the fields the engine actually checks (`includeRuleIds`, `tags`, etc.).
|
|
5
|
+
`runOnly` must be an object, not a bare array — `runOnly: ['img-alt-present']` is silently ignored (the engine falls through to "run everything"), because that shape has none of the fields the engine actually checks (`includeRuleIds`, `tags`, etc.). A bare array is easy to reach for, so this is worth checking first.
|
|
6
6
|
|
|
7
7
|
Fix:
|
|
8
8
|
|
|
@@ -41,7 +41,7 @@ Treat it as "needs a human to look" — it's neither pass nor fail by design. Mo
|
|
|
41
41
|
|
|
42
42
|
## "What happens if a locale is only partially translated?"
|
|
43
43
|
|
|
44
|
-
Missing keys fall back to English per-string (never a blank or broken result), so a partial locale degrades gracefully rather than failing outright — see [`I18N.md`](./I18N.md) for the mechanism and current coverage.
|
|
44
|
+
Missing keys fall back to English per-string (never a blank or broken result), so a partial locale degrades gracefully rather than failing outright — see [`I18N.md`](./I18N.md) for the mechanism and current coverage. Key parity is enforced rather than hoped for: `npm run i18n:sync` carries any new or renamed `en.json` key into every other locale file, and the build fails if one is out of step. A key it adds holds the English text until someone translates it, so a locale can be behind on wording without ever being behind on keys.
|
|
45
45
|
|
|
46
46
|
## "`runDomRulesInPage` vs `runa11yCoreInPage` — which one do I want?"
|
|
47
47
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surea11y/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Deterministic WCAG 2.2 accessibility engine that tells you what it can't tell you. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"src/checks/**/*.js",
|
|
62
62
|
"bin/surea11y-core.js",
|
|
63
63
|
"surea11y.browser.js",
|
|
64
|
+
"surea11y.i18n.*.js",
|
|
64
65
|
"docs/**/*.md",
|
|
65
66
|
"README.md",
|
|
66
67
|
"LICENSE",
|
|
@@ -87,19 +88,25 @@
|
|
|
87
88
|
"test:typography-helpers": "node tests/contrast-typography.test.js",
|
|
88
89
|
"coverage": "node scripts/generate-wcag-coverage.js --rulesDir src/checks",
|
|
89
90
|
"coverage:strict": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --strictFacets",
|
|
91
|
+
"coverage:check": "node scripts/generate-wcag-coverage.js --rulesDir src/checks --check",
|
|
90
92
|
"fixtures:index": "node scripts/generate-fixture-index.js",
|
|
91
93
|
"docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
|
|
92
94
|
"validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic",
|
|
93
95
|
"validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual",
|
|
96
|
+
"validate:rules": "npm run validate:automatic-rules && npm run validate:manual-rules",
|
|
94
97
|
"i18n:new": "node scripts/i18n-scaffold.js",
|
|
98
|
+
"i18n:sync": "node scripts/i18n-sync.js",
|
|
99
|
+
"i18n:check": "node scripts/i18n-sync.js --check",
|
|
95
100
|
"i18n:report": "node scripts/i18n-report.js"
|
|
96
101
|
},
|
|
97
102
|
"devDependencies": {
|
|
98
103
|
"@eslint/js": "^10.0.1",
|
|
104
|
+
"aria-query": "^5.3.2",
|
|
99
105
|
"eslint": "^10.8.0",
|
|
100
106
|
"eslint-config-prettier": "^10.1.8",
|
|
101
107
|
"globals": "^17.8.0",
|
|
102
108
|
"jsdom": "^29.1.1",
|
|
109
|
+
"language-subtag-registry": "^0.4.2",
|
|
103
110
|
"playwright": "^1.62.1",
|
|
104
111
|
"prettier": "^3.9.6"
|
|
105
112
|
}
|