@surea11y/core 1.3.0 → 1.4.1
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 +87 -2
- package/README.md +109 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +26 -0
- package/docs/ARIA_DEPRECATION.md +95 -0
- package/docs/CI_INTEGRATIONS.md +7 -7
- package/docs/ENGINE_OPTIONS.md +1 -1
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +6 -6
- package/package.json +52 -16
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +4 -6
- package/src/checks/automatic/aria-allowed-attr.js +663 -134
- package/src/checks/automatic/aria-allowed-role.js +2 -0
- package/src/checks/automatic/aria-braille-equivalent.js +2 -0
- package/src/checks/automatic/aria-conditional-attr.js +8 -7
- package/src/checks/automatic/aria-deprecated-role.js +107 -38
- package/src/checks/automatic/aria-hidden-body.js +6 -4
- package/src/checks/automatic/aria-hidden-focus.js +12 -13
- package/src/checks/automatic/aria-prohibited-attr.js +98 -105
- package/src/checks/automatic/aria-prohibited-children.js +56 -87
- package/src/checks/automatic/aria-required-attr.js +6 -7
- package/src/checks/automatic/aria-required-children.js +7 -10
- package/src/checks/automatic/aria-required-parent.js +20 -25
- package/src/checks/automatic/aria-role-name-present.js +2 -0
- package/src/checks/automatic/aria-roles-valid.js +33 -6
- package/src/checks/automatic/aria-valid-attr-value.js +18 -15
- package/src/checks/automatic/aria-valid-attr.js +2 -0
- package/src/checks/automatic/autocomplete-valid.js +39 -1
- package/src/checks/automatic/avoid-inline-spacing.js +181 -20
- package/src/checks/automatic/binary-control-name-present.js +13 -3
- package/src/checks/automatic/button-name-present.js +54 -21
- package/src/checks/automatic/canvas-text-alternative-present.js +17 -8
- package/src/checks/automatic/combobox-name-present.js +10 -1
- package/src/checks/automatic/contrast-computable.js +2 -0
- package/src/checks/automatic/contrast-enhanced.js +2 -0
- package/src/checks/automatic/contrast-minimum.js +2 -0
- package/src/checks/automatic/css-orientation-lock.js +21 -27
- package/src/checks/automatic/definition-list-children-valid.js +6 -6
- package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
- package/src/checks/automatic/dialog-name-present.js +18 -11
- package/src/checks/automatic/dlitem-parent-valid.js +2 -0
- package/src/checks/automatic/duplicate-id-aria.js +4 -3
- package/src/checks/automatic/embed-text-alternative-present.js +2 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +50 -4
- package/src/checks/automatic/form-control-single-label.js +110 -43
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
- package/src/checks/automatic/iframe-focusable-content.js +246 -18
- package/src/checks/automatic/iframe-name-present.js +2 -0
- package/src/checks/automatic/iframe-title-unique.js +3 -1
- package/src/checks/automatic/img-alt-present.js +23 -18
- package/src/checks/automatic/input-image-alt-present.js +101 -52
- package/src/checks/automatic/label-in-name.js +97 -27
- package/src/checks/automatic/language-page-present.js +7 -1
- package/src/checks/automatic/link-in-text-block.js +2 -0
- package/src/checks/automatic/link-name-present.js +52 -17
- package/src/checks/automatic/list-children-valid.js +14 -24
- package/src/checks/automatic/listbox-name-present.js +10 -1
- package/src/checks/automatic/listitem-parent-valid.js +30 -7
- package/src/checks/automatic/menuitem-name-present.js +10 -1
- package/src/checks/automatic/meta-refresh-no-exceptions.js +33 -4
- package/src/checks/automatic/meta-refresh-timing-absent.js +32 -4
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +39 -15
- package/src/checks/automatic/meter-name-present.js +12 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +177 -25
- package/src/checks/automatic/object-text-alternative-present.js +16 -7
- package/src/checks/automatic/option-name-present.js +10 -1
- package/src/checks/automatic/page-title-present.js +2 -0
- package/src/checks/automatic/progressbar-name-present.js +16 -11
- package/src/checks/automatic/role-img-alt-present.js +4 -4
- package/src/checks/automatic/searchbox-name-present.js +10 -1
- package/src/checks/automatic/server-side-image-map-absent.js +4 -3
- package/src/checks/automatic/slider-name-present.js +13 -2
- package/src/checks/automatic/spinbutton-name-present.js +10 -1
- package/src/checks/automatic/summary-name-present.js +10 -1
- package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
- package/src/checks/automatic/svg-text-alternative-present.js +17 -5
- package/src/checks/automatic/tab-name-present.js +10 -1
- package/src/checks/automatic/table-headers-attr-valid.js +3 -2
- package/src/checks/automatic/table-th-has-data-cells.js +69 -6
- package/src/checks/automatic/target-size-minimum.js +5 -0
- package/src/checks/automatic/td-has-header.js +24 -1
- package/src/checks/automatic/textbox-name-present.js +10 -1
- package/src/checks/automatic/tooltip-name-present.js +10 -1
- package/src/checks/automatic/treeitem-name-present.js +10 -1
- package/src/checks/automatic/valid-lang.js +18 -3
- package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
- package/src/checks/manual/accesskeys-manual.js +3 -1
- package/src/checks/manual/area-alt-decorative-manual.js +2 -0
- package/src/checks/manual/area-alt-quality-manual.js +2 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
- package/src/checks/manual/aria-text-manual.js +6 -5
- package/src/checks/manual/bypass-blocks-present-manual.js +279 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/css-hidden-focus.js +184 -9
- package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
- package/src/checks/manual/empty-heading-manual.js +17 -17
- package/src/checks/manual/empty-table-header-manual.js +52 -25
- package/src/checks/manual/focus-order-semantics-manual.js +16 -4
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
- package/src/checks/manual/heading-order-manual.js +28 -1
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
- package/src/checks/manual/image-redundant-alt-manual.js +21 -1
- package/src/checks/manual/img-alt-decorative-manual.js +2 -0
- package/src/checks/manual/img-alt-quality-manual.js +2 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +26 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
- package/src/checks/manual/label-title-only-manual.js +29 -22
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +51 -55
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +44 -31
- package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
- package/src/checks/manual/landmark-one-main-manual.js +12 -23
- package/src/checks/manual/landmark-unique-manual.js +37 -52
- package/src/checks/manual/link-name-quality-manual.js +2 -0
- package/src/checks/manual/media-transcript-present-manual.js +2 -0
- package/src/checks/manual/meta-viewport-large-manual.js +3 -1
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/p-as-heading-manual.js +2 -0
- package/src/checks/manual/page-has-heading-one-manual.js +12 -11
- package/src/checks/manual/page-title-patterns-manual.js +2 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
- package/src/checks/manual/region-manual.js +27 -36
- package/src/checks/manual/scope-attr-valid-manual.js +3 -1
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
- package/src/checks/manual/skip-link-manual.js +7 -6
- package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/tabindex-manual.js +3 -1
- package/src/checks/manual/table-duplicate-name-manual.js +5 -4
- package/src/checks/manual/table-fake-caption-manual.js +24 -3
- package/src/checks/manual/video-caption-manual.js +2 -0
- package/src/checks/manual-review.js +2 -0
- package/src/core.js +11820 -3317
- package/src/index.js +2 -0
- package/src/report.js +51 -9
- package/src/sarif.js +20 -5
- package/surea11y.browser.js +4943 -1388
- package/bin/core.js +0 -473
- package/docs/CLI.md +0 -128
- package/src/catalogs/composites.wcag.js +0 -454
- package/src/checks/automatic/bypass-blocks-present.js +0 -215
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -1211
- package/src/core/contrast-helpers.js +0 -1302
- package/src/core/dom-helpers.js +0 -4493
- package/src/core/dom-runner.js +0 -787
- package/src/core/frame-messaging.js +0 -261
- package/src/core/frame-scan.js +0 -190
- package/src/core/rollup-composites.js +0 -127
- package/src/core/rule-meta.js +0 -176
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -1228
- package/src/i18n/fr.js +0 -1185
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -59
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
package/docs/CI_INTEGRATIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CI/CD pipeline integrations
|
|
2
2
|
|
|
3
|
-
Ready-to-paste templates wrapping the [CLI](
|
|
3
|
+
Ready-to-paste templates wrapping the [CLI](https://github.com/SureA11y/cli#readme) (`npx @surea11y/cli scan ...`) in GitHub Actions and Bitbucket Pipelines. If you're calling the library directly from your own Node script instead of the CLI, see [`INTEGRATION.md`](./INTEGRATION.md#ci-gating-a-build-on-the-result) instead — this page is specifically about the CLI as a pipeline step.
|
|
4
4
|
|
|
5
5
|
All of these rely on the CLI's own exit codes (`0` clean, `1` at least one — or one *new*, with `--baseline` — `fail` outcome, `2` a usage/scan error) to gate the pipeline; no extra scripting is required for basic pass/fail gating.
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
with:
|
|
22
22
|
node-version: 20
|
|
23
23
|
- run: npm ci && npm run build # produce whatever static HTML you're scanning
|
|
24
|
-
- run: npx @surea11y/
|
|
24
|
+
- run: npx @surea11y/cli scan ./dist/index.html
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
This fails the job the moment any `fail` outcome is found. For an existing site with pre-existing violations, see the baseline variant below instead of disabling the step.
|
|
@@ -30,13 +30,13 @@ This fails the job the moment any `fail` outcome is found. For an existing site
|
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
32
|
# Once, locally: record every current fail occurrence, commit the file.
|
|
33
|
-
npx @surea11y/
|
|
33
|
+
npx @surea11y/cli scan ./dist/index.html --write-baseline a11y-baseline.json
|
|
34
34
|
git add a11y-baseline.json
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
```yaml
|
|
38
38
|
- run: npm ci && npm run build
|
|
39
|
-
- run: npx @surea11y/
|
|
39
|
+
- run: npx @surea11y/cli scan ./dist/index.html --baseline a11y-baseline.json
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
See [`BASELINE.md`](./BASELINE.md) for what counts as "known" vs. "new", and how to regenerate the file as violations get fixed.
|
|
@@ -63,7 +63,7 @@ jobs:
|
|
|
63
63
|
node-version: 20
|
|
64
64
|
- run: npm ci && npm run build
|
|
65
65
|
- name: Scan (report, don't fail the job here)
|
|
66
|
-
run: npx @surea11y/
|
|
66
|
+
run: npx @surea11y/cli scan ./dist/index.html --baseline a11y-baseline.json --sarif results.sarif
|
|
67
67
|
continue-on-error: true
|
|
68
68
|
id: scan
|
|
69
69
|
- name: Upload SARIF to Code Scanning
|
|
@@ -91,7 +91,7 @@ pipelines:
|
|
|
91
91
|
script:
|
|
92
92
|
- npm ci
|
|
93
93
|
- npm run build
|
|
94
|
-
- npx @surea11y/
|
|
94
|
+
- npx @surea11y/cli scan ./dist/index.html --baseline a11y-baseline.json --html a11y-report.html
|
|
95
95
|
artifacts:
|
|
96
96
|
- a11y-report.html
|
|
97
97
|
```
|
|
@@ -100,4 +100,4 @@ The step fails the pipeline on the CLI's exit code exactly like any other `scrip
|
|
|
100
100
|
|
|
101
101
|
## Free-tier/private-repo minute limits
|
|
102
102
|
|
|
103
|
-
If your pipeline provider's free tier is minute-limited (Bitbucket Pipelines' free tier is 50 build-minutes/month on private workspaces, for example), a `jsdom`-based scan of static/server-rendered HTML (what the CLI does) is far cheaper than driving a real browser — see [
|
|
103
|
+
If your pipeline provider's free tier is minute-limited (Bitbucket Pipelines' free tier is 50 build-minutes/month on private workspaces, for example), a `jsdom`-based scan of static/server-rendered HTML (what the CLI does) is far cheaper than driving a real browser — see [the CLI docs](https://github.com/SureA11y/cli/blob/main/docs/CLI.md#what-it-can-and-cant-scan) for what that trades away (no client-rendered content, no real CSS layout).
|
package/docs/ENGINE_OPTIONS.md
CHANGED
|
@@ -224,7 +224,7 @@ See the option-by-option table above for anything not shown here, and the `custo
|
|
|
224
224
|
|
|
225
225
|
Every shipped rule is baked into `src/core.js` at build time. `engineOptions.customRules` is the runtime escape hatch: an array of rule descriptors registered for that one call only — nothing is added to the static catalog (`getRulesCatalog()`/`getChecksCatalog()`), and nothing persists between calls. This is deliberate, not a limitation to work around: surea11y already takes fresh `engineOptions` per call with no mutable global config (unlike some other engines, which need a `configure()`/`reset()` step against a shared runtime), and custom rules follow that same per-call model.
|
|
226
226
|
|
|
227
|
-
Calling the library directly is one way in; the CLI also exposes this via `--custom-rules <path>` (a local file, loaded once per scan) — see [
|
|
227
|
+
Calling the library directly is one way in; the CLI also exposes this via `--custom-rules <path>` (a local file, loaded once per scan) — see [the CLI docs](https://github.com/SureA11y/cli/blob/main/docs/CLI.md#custom-rules).
|
|
228
228
|
|
|
229
229
|
A descriptor has the *same shape as an internal rule module's own export* — if you already know how to write a rule file for this engine, you already know this API:
|
|
230
230
|
|
package/docs/I18N.md
CHANGED
|
@@ -6,10 +6,12 @@ Every rule's title/description, and every occurrence's summary/hint, is localize
|
|
|
6
6
|
|
|
7
7
|
| Locale | File | Keys | Coverage vs. English |
|
|
8
8
|
|---|---|---|---|
|
|
9
|
-
| `en` (English) | `src/i18n/en.js` |
|
|
10
|
-
| `fr` (French) | `src/i18n/fr.js` |
|
|
9
|
+
| `en` (English) | `src/i18n/en.js` | 614 | 100% (the canonical/fallback set) |
|
|
10
|
+
| `fr` (French) | `src/i18n/fr.js` | 614 | 100% |
|
|
11
|
+
| `de` (German) | `src/i18n/de.js` | 614 | 100% |
|
|
12
|
+
| `es` (Spanish) | `src/i18n/es.js` | 614 | 100% |
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
All four locales are fully translated as of this writing. That won't stay automatically true — every time a new rule (or a new i18n key) is added to `en.js`, every other locale needs the same key added, or it silently falls back to English for that string (see the fallback behavior below). `tests/i18n/i18n-locale-completeness.test.js` catches this: it fails the build if a locale listed in `FULLY_TRANSLATED_LOCALES` (currently `fr`, `de`, `es`) is missing any key present in `en.js`, and fails for *any* locale that has an orphaned key not in `en.js` (a sign of a typo or a stale key left behind after a rule was removed). Run `npm run i18n:report` any time to see per-locale coverage.
|
|
13
15
|
|
|
14
16
|
## Selecting a locale
|
|
15
17
|
|
|
@@ -17,7 +19,7 @@ Both locales are fully translated as of this writing. That won't stay automatica
|
|
|
17
19
|
runDomRulesInPage(url, null, { locale: 'fr' }, null);
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
Default is `'en'` if omitted. Any string is accepted — an unrecognized locale (e.g. `'
|
|
22
|
+
Default is `'en'` if omitted. Any string is accepted — an unrecognized locale (e.g. `'ja'`, not yet built) behaves exactly like a locale that's 0% translated: every string falls through to English (see below), not an error.
|
|
21
23
|
|
|
22
24
|
## Fallback behavior (per-string, not per-locale)
|
|
23
25
|
|
|
@@ -40,8 +42,9 @@ Both are included in the result alongside the already-resolved text, so you can
|
|
|
40
42
|
|
|
41
43
|
## Contributing a translation
|
|
42
44
|
|
|
43
|
-
1.
|
|
44
|
-
2.
|
|
45
|
-
3.
|
|
46
|
-
4.
|
|
47
|
-
5.
|
|
45
|
+
1. Scaffold the file: `npm run i18n:new <locale>` (e.g. `npm run i18n:new de`) creates `src/i18n/<locale>.js` with all of `en.js`'s keys already present, each seeded with the English text as a placeholder. It refuses to overwrite an existing locale file unless you pass `--force`.
|
|
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.
|
package/docs/INTEGRATION.md
CHANGED
|
@@ -130,7 +130,7 @@ if (failures.length > 0) {
|
|
|
130
130
|
|
|
131
131
|
Notes for CI specifically:
|
|
132
132
|
- `cantTell` outcomes are advisory by design (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md#outcome-values)) — most teams log them without failing the build, since they require human judgment the CI run can't make.
|
|
133
|
-
- For "only fail on *new* violations," the CLI has a built-in baseline/allowlist mechanism (`--write-baseline`/`--baseline`, see [`BASELINE.md`](./BASELINE.md)). Calling the library directly, the same matching logic is available as `buildBaselineEntries(result)`/`matchBaseline(result, baselineEntries)` from `require('@surea11y/core/
|
|
133
|
+
- For "only fail on *new* violations," the CLI has a built-in baseline/allowlist mechanism (`--write-baseline`/`--baseline`, see [`BASELINE.md`](./BASELINE.md)). Calling the library directly, the same matching logic is available as `buildBaselineEntries(result)`/`matchBaseline(result, baselineEntries)` from `require('@surea11y/core/baseline')` — or diff `checksResults` against a saved prior run yourself if your pages don't fit that model (see `BASELINE.md`'s "known limitation").
|
|
134
134
|
- Prefer Pattern 1 (jsdom) in CI unless you specifically need real-browser layout — it avoids the extra weight of a Puppeteer/Playwright + browser-binary install in your pipeline.
|
|
135
135
|
|
|
136
136
|
## Browser extension context
|
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/
|
|
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. Other engines running inside an actual loaded browser tab see the post-hydration state instead, so the two can disagree on exactly this class of element for reasons that have nothing to do with either engine's 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/REPORT.md
CHANGED
|
@@ -18,7 +18,7 @@ Open `report.html` directly from disk. Works alongside any other output mode —
|
|
|
18
18
|
## Library usage
|
|
19
19
|
|
|
20
20
|
```js
|
|
21
|
-
const { renderHtmlReport } = require('@surea11y/core/
|
|
21
|
+
const { renderHtmlReport } = require('@surea11y/core/report');
|
|
22
22
|
const { runDomRulesInPage } = require('@surea11y/core');
|
|
23
23
|
|
|
24
24
|
const result = runDomRulesInPage(url, null, {}, null);
|
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/package.json
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surea11y/core",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "Deterministic WCAG 2.2 accessibility engine that tells you what it can't tell you. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"accessibility",
|
|
7
|
+
"a11y",
|
|
8
|
+
"wcag",
|
|
9
|
+
"wcag21",
|
|
10
|
+
"wcag22",
|
|
11
|
+
"accessibility-testing",
|
|
12
|
+
"a11y-testing",
|
|
13
|
+
"audit",
|
|
14
|
+
"aria",
|
|
15
|
+
"contrast",
|
|
16
|
+
"sarif",
|
|
17
|
+
"zero-dependencies",
|
|
18
|
+
"jsdom",
|
|
19
|
+
"playwright",
|
|
20
|
+
"puppeteer",
|
|
21
|
+
"selenium",
|
|
22
|
+
"cypress",
|
|
23
|
+
"webdriverio",
|
|
24
|
+
"jest",
|
|
25
|
+
"vitest"
|
|
26
|
+
],
|
|
5
27
|
"main": "src/index.js",
|
|
6
28
|
"bin": {
|
|
7
|
-
"surea11y": "bin/core.js"
|
|
29
|
+
"surea11y-core": "bin/surea11y-core.js"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./src/index.js",
|
|
33
|
+
"./baseline": "./src/baseline.js",
|
|
34
|
+
"./report": "./src/report.js",
|
|
35
|
+
"./sarif": "./src/sarif.js",
|
|
36
|
+
"./browser": "./surea11y.browser.js",
|
|
37
|
+
"./package.json": "./package.json"
|
|
8
38
|
},
|
|
9
39
|
"author": "Jorge Rumoroso",
|
|
10
40
|
"license": "MPL-2.0",
|
|
11
41
|
"repository": {
|
|
12
42
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/
|
|
43
|
+
"url": "git+https://github.com/SureA11y/core.git"
|
|
14
44
|
},
|
|
15
|
-
"homepage": "https://github.com/
|
|
45
|
+
"homepage": "https://github.com/SureA11y/core#readme",
|
|
16
46
|
"bugs": {
|
|
17
|
-
"url": "https://github.com/
|
|
47
|
+
"url": "https://github.com/SureA11y/core/issues"
|
|
18
48
|
},
|
|
19
49
|
"engines": {
|
|
20
50
|
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
|
@@ -23,8 +53,13 @@
|
|
|
23
53
|
"access": "public"
|
|
24
54
|
},
|
|
25
55
|
"files": [
|
|
26
|
-
"src",
|
|
27
|
-
"
|
|
56
|
+
"src/index.js",
|
|
57
|
+
"src/core.js",
|
|
58
|
+
"src/baseline.js",
|
|
59
|
+
"src/report.js",
|
|
60
|
+
"src/sarif.js",
|
|
61
|
+
"src/checks/**/*.js",
|
|
62
|
+
"bin/surea11y-core.js",
|
|
28
63
|
"surea11y.browser.js",
|
|
29
64
|
"docs/**/*.md",
|
|
30
65
|
"README.md",
|
|
@@ -33,8 +68,7 @@
|
|
|
33
68
|
"!docs/RULE_TEMPLATE.md",
|
|
34
69
|
"!docs/RULE_TEST_TEMPLATE.md",
|
|
35
70
|
"!docs/RULE_TEST_AUTHORING.md",
|
|
36
|
-
"!docs/TEST_OUTCOME_STABILITY.md"
|
|
37
|
-
"!src/explain"
|
|
71
|
+
"!docs/TEST_OUTCOME_STABILITY.md"
|
|
38
72
|
],
|
|
39
73
|
"scripts": {
|
|
40
74
|
"lint": "eslint .",
|
|
@@ -43,8 +77,8 @@
|
|
|
43
77
|
"format:check": "prettier --check \"src/**/*.js\" \"bin/**/*.js\" \"scripts/**/*.js\" \"tests/**/*.js\" \"eslint.config.js\"",
|
|
44
78
|
"build": "node scripts/build-core.js && node scripts/build-browser.js",
|
|
45
79
|
"pretest": "playwright install chromium",
|
|
46
|
-
"test": "npm run build && node scripts/run-tests.js",
|
|
47
|
-
"test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js
|
|
80
|
+
"test": "npm run format:check && npm run build && node scripts/run-tests.js",
|
|
81
|
+
"test:coverage": "npm run build && node scripts/run-tests.js --experimental-test-coverage --test-coverage-include=src/**/*.js",
|
|
48
82
|
"test:contrast-helpers": "node tests/contrast-helpers.test.js",
|
|
49
83
|
"helpers-perf-bench": "node --expose-gc scripts/dom-helpers-perf-bench.js",
|
|
50
84
|
"engine-perf-bench": "node --expose-gc scripts/engine-perf-bench.js --profileRules=true --iters=25 --top=15",
|
|
@@ -56,16 +90,18 @@
|
|
|
56
90
|
"fixtures:index": "node scripts/generate-fixture-index.js",
|
|
57
91
|
"docs:rule-catalog": "npm run build && node scripts/generate-rule-catalog.js",
|
|
58
92
|
"validate:automatic-rules": "npm run build && node scripts/validate-all-rules.js src/checks/automatic",
|
|
59
|
-
"validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"jsdom": "^29.1.1"
|
|
93
|
+
"validate:manual-rules": "npm run build && node scripts/validate-all-rules.js src/checks/manual",
|
|
94
|
+
"i18n:new": "node scripts/i18n-scaffold.js",
|
|
95
|
+
"i18n:report": "node scripts/i18n-report.js"
|
|
63
96
|
},
|
|
64
97
|
"devDependencies": {
|
|
65
98
|
"@eslint/js": "^10.0.1",
|
|
99
|
+
"aria-query": "^5.3.2",
|
|
66
100
|
"eslint": "^10.8.0",
|
|
67
101
|
"eslint-config-prettier": "^10.1.8",
|
|
68
102
|
"globals": "^17.8.0",
|
|
103
|
+
"jsdom": "^29.1.1",
|
|
104
|
+
"language-subtag-registry": "^0.4.2",
|
|
69
105
|
"playwright": "^1.62.1",
|
|
70
106
|
"prettier": "^3.9.6"
|
|
71
107
|
}
|
package/src/baseline.js
CHANGED
|
Binary file
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: MPL-2.0 */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -199,12 +201,8 @@ function runInPage(ctx) {
|
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
// A non-empty title attribute is HTML-AAM's own next fallback naming
|
|
202
|
-
// source once alt is entirely absent
|
|
203
|
-
//
|
|
204
|
-
// non-empty-alt/aria-label/aria-labelledby). See img-alt-present's
|
|
205
|
-
// sibling fix (2026-07-23, AliExpress's title-only logo <img>) for
|
|
206
|
-
// the real page this was found via -- same gap, same fix, different
|
|
207
|
-
// element.
|
|
204
|
+
// source once alt is entirely absent. Same gap img-alt-present handles
|
|
205
|
+
// for <img title="..."> with no alt.
|
|
208
206
|
const titleRaw = (() => {
|
|
209
207
|
try {
|
|
210
208
|
return el.getAttribute('title');
|