@surea11y/core 1.0.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 +49 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/core.js +244 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +41 -0
- package/docs/CLI.md +49 -0
- package/docs/ENGINE_OPTIONS.md +155 -0
- package/docs/I18N.md +47 -0
- package/docs/INTEGRATION.md +156 -0
- package/docs/LIMITATIONS.md +31 -0
- package/docs/OUTPUT_SCHEMA.md +237 -0
- package/docs/POLICY.md +71 -0
- package/docs/RULE_AUTHORING.md +375 -0
- package/docs/RULE_CATALOG.md +180 -0
- package/docs/RULE_TAXONOMY.md +145 -0
- package/docs/TROUBLESHOOTING.md +48 -0
- package/docs/WCAG_CONFORMANCE.md +49 -0
- package/package.json +60 -0
- package/src/catalogs/composites.wcag.js +490 -0
- package/src/checks/automatic/area-alt-present.js +225 -0
- package/src/checks/automatic/aria-allowed-attr.js +206 -0
- package/src/checks/automatic/aria-allowed-role.js +102 -0
- package/src/checks/automatic/aria-braille-equivalent.js +139 -0
- package/src/checks/automatic/aria-conditional-attr.js +110 -0
- package/src/checks/automatic/aria-deprecated-role.js +106 -0
- package/src/checks/automatic/aria-hidden-body.js +87 -0
- package/src/checks/automatic/aria-hidden-focus.js +480 -0
- package/src/checks/automatic/aria-prohibited-attr.js +156 -0
- package/src/checks/automatic/aria-prohibited-children.js +265 -0
- package/src/checks/automatic/aria-required-attr.js +154 -0
- package/src/checks/automatic/aria-required-children.js +274 -0
- package/src/checks/automatic/aria-required-parent.js +222 -0
- package/src/checks/automatic/aria-role-name-present.js +201 -0
- package/src/checks/automatic/aria-roles-valid.js +110 -0
- package/src/checks/automatic/aria-valid-attr-value.js +123 -0
- package/src/checks/automatic/aria-valid-attr.js +109 -0
- package/src/checks/automatic/autocomplete-valid.js +134 -0
- package/src/checks/automatic/avoid-inline-spacing.js +107 -0
- package/src/checks/automatic/binary-control-name-present.js +294 -0
- package/src/checks/automatic/button-name-present.js +146 -0
- package/src/checks/automatic/bypass-blocks-present.js +162 -0
- package/src/checks/automatic/canvas-text-alternative-present.js +140 -0
- package/src/checks/automatic/combobox-name-present.js +267 -0
- package/src/checks/automatic/contrast-computable.js +378 -0
- package/src/checks/automatic/contrast-enhanced.js +517 -0
- package/src/checks/automatic/contrast-minimum.js +512 -0
- package/src/checks/automatic/css-orientation-lock.js +206 -0
- package/src/checks/automatic/definition-list-children-valid.js +148 -0
- package/src/checks/automatic/deprecated-elements-not-used.js +91 -0
- package/src/checks/automatic/dialog-name-present.js +209 -0
- package/src/checks/automatic/dlitem-parent-valid.js +100 -0
- package/src/checks/automatic/duplicate-id-aria.js +126 -0
- package/src/checks/automatic/embed-text-alternative-present.js +190 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +409 -0
- package/src/checks/automatic/form-control-single-label.js +117 -0
- package/src/checks/automatic/html-xml-lang-mismatch.js +91 -0
- package/src/checks/automatic/iframe-focusable-content.js +141 -0
- package/src/checks/automatic/iframe-name-present.js +102 -0
- package/src/checks/automatic/iframe-title-unique.js +107 -0
- package/src/checks/automatic/img-alt-present.js +223 -0
- package/src/checks/automatic/input-image-alt-present.js +155 -0
- package/src/checks/automatic/label-in-name.js +326 -0
- package/src/checks/automatic/language-page-present.js +159 -0
- package/src/checks/automatic/link-in-text-block.js +218 -0
- package/src/checks/automatic/link-name-present.js +114 -0
- package/src/checks/automatic/list-children-valid.js +152 -0
- package/src/checks/automatic/listbox-name-present.js +236 -0
- package/src/checks/automatic/listitem-parent-valid.js +118 -0
- package/src/checks/automatic/menuitem-name-present.js +201 -0
- package/src/checks/automatic/meta-refresh-no-exceptions.js +105 -0
- package/src/checks/automatic/meta-refresh-timing-absent.js +107 -0
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +118 -0
- package/src/checks/automatic/meter-name-present.js +160 -0
- package/src/checks/automatic/nested-interactive-controls-absent.js +135 -0
- package/src/checks/automatic/object-text-alternative-present.js +193 -0
- package/src/checks/automatic/option-name-present.js +157 -0
- package/src/checks/automatic/page-title-present.js +86 -0
- package/src/checks/automatic/progressbar-name-present.js +165 -0
- package/src/checks/automatic/role-img-alt-present.js +206 -0
- package/src/checks/automatic/searchbox-name-present.js +236 -0
- package/src/checks/automatic/server-side-image-map-absent.js +88 -0
- package/src/checks/automatic/slider-name-present.js +276 -0
- package/src/checks/automatic/spinbutton-name-present.js +236 -0
- package/src/checks/automatic/summary-name-present.js +153 -0
- package/src/checks/automatic/svg-image-text-alternative-present.js +220 -0
- package/src/checks/automatic/svg-text-alternative-present.js +298 -0
- package/src/checks/automatic/tab-name-present.js +200 -0
- package/src/checks/automatic/table-headers-attr-valid.js +122 -0
- package/src/checks/automatic/table-th-has-data-cells.js +117 -0
- package/src/checks/automatic/target-size-minimum.js +605 -0
- package/src/checks/automatic/td-has-header.js +151 -0
- package/src/checks/automatic/textbox-name-present.js +236 -0
- package/src/checks/automatic/tooltip-name-present.js +158 -0
- package/src/checks/automatic/treeitem-name-present.js +157 -0
- package/src/checks/automatic/valid-lang.js +100 -0
- package/src/checks/automatic/video-poster-text-alternative-present.js +193 -0
- package/src/checks/manual/accesskeys-manual.js +93 -0
- package/src/checks/manual/area-alt-decorative-manual.js +247 -0
- package/src/checks/manual/area-alt-quality-manual.js +204 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +141 -0
- package/src/checks/manual/aria-text-manual.js +109 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +170 -0
- package/src/checks/manual/css-hidden-focus.js +259 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +204 -0
- package/src/checks/manual/empty-heading-manual.js +182 -0
- package/src/checks/manual/empty-table-header-manual.js +163 -0
- package/src/checks/manual/focus-order-semantics-manual.js +117 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +291 -0
- package/src/checks/manual/heading-order-manual.js +130 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +142 -0
- package/src/checks/manual/image-redundant-alt-manual.js +118 -0
- package/src/checks/manual/img-alt-decorative-manual.js +148 -0
- package/src/checks/manual/img-alt-quality-manual.js +182 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +144 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +144 -0
- package/src/checks/manual/label-title-only-manual.js +115 -0
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +180 -0
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +169 -0
- package/src/checks/manual/landmark-main-is-top-level-manual.js +167 -0
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +177 -0
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +169 -0
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +132 -0
- package/src/checks/manual/landmark-one-main-manual.js +151 -0
- package/src/checks/manual/landmark-unique-manual.js +252 -0
- package/src/checks/manual/link-name-quality-manual.js +143 -0
- package/src/checks/manual/media-transcript-present-manual.js +373 -0
- package/src/checks/manual/meta-viewport-large-manual.js +119 -0
- package/src/checks/manual/mouse-only-event-handlers-manual.js +134 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +116 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +194 -0
- package/src/checks/manual/p-as-heading-manual.js +163 -0
- package/src/checks/manual/page-has-heading-one-manual.js +110 -0
- package/src/checks/manual/page-title-patterns-manual.js +262 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +159 -0
- package/src/checks/manual/region-manual.js +183 -0
- package/src/checks/manual/scope-attr-valid-manual.js +93 -0
- package/src/checks/manual/scrollable-region-focusable-manual.js +168 -0
- package/src/checks/manual/skip-link-manual.js +150 -0
- package/src/checks/manual/svg-text-alternative-quality-manual.js +209 -0
- package/src/checks/manual/tabindex-manual.js +94 -0
- package/src/checks/manual/table-duplicate-name-manual.js +99 -0
- package/src/checks/manual/table-fake-caption-manual.js +122 -0
- package/src/checks/manual/video-caption-manual.js +118 -0
- package/src/checks/manual-review.js +95 -0
- package/src/checks/rules-and-tags.full.csv +19 -0
- package/src/checks/rules-and-tags.full.json +259 -0
- package/src/core/aria-helpers.js +906 -0
- package/src/core/contrast-helpers.js +1147 -0
- package/src/core/dom-helpers.js +4085 -0
- package/src/core/dom-runner.js +627 -0
- package/src/core/frame-messaging.js +210 -0
- package/src/core/frame-scan.js +178 -0
- package/src/core/rollup-composites.js +135 -0
- package/src/core/rule-meta.js +140 -0
- package/src/core.js +79055 -0
- package/src/coverage/wcag-facets.js +1079 -0
- package/src/coverage/wcag-version-map.js +84 -0
- package/src/i18n/en.js +919 -0
- package/src/i18n/fr.js +527 -0
- package/src/index.js +4 -0
- package/src/policy/contracts.js +18 -0
- package/src/policy/resolvePolicy.js +55 -0
- package/src/policy/schemas/engine-options.schema.json +103 -0
- package/src/policy/schemas/policy-contract.schema.json +40 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Engine options reference
|
|
2
|
+
|
|
3
|
+
Every runner (`runDomRulesInPage`, `runa11yCoreInPage`) takes the same four arguments: `(pageUrl, contextSelector, engineOptions, runOnly)`. This page documents `engineOptions` and `runOnly` in full — the real, current surface, verified against `src/core/dom-runner.js` and `scripts/build-core.js`, not the historical `docs/README.md`.
|
|
4
|
+
|
|
5
|
+
## Selecting which rules run
|
|
6
|
+
|
|
7
|
+
There are **two independent ways** to select rules — the 4th argument (`runOnly`), or `engineOptions.rules`/`.tags`/`.tests`/`.includeMode`. If `runOnly` contains any filter, it wins outright; otherwise the engine falls back to `engineOptions`. Don't mix them expecting both to apply — pick one.
|
|
8
|
+
|
|
9
|
+
### Via `runOnly` (4th argument)
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
runDomRulesInPage(url, null, {}, {
|
|
13
|
+
includeRuleIds: ['img-alt-present', 'button-name-present'],
|
|
14
|
+
excludeRuleIds: ['region'],
|
|
15
|
+
tags: ['wcag412'],
|
|
16
|
+
excludeTags: ['best-practice'],
|
|
17
|
+
includeMode: 'and' // 'and' (default) | 'or' — see below
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> ⚠️ **`runOnly` must be this object shape, not a bare array.** `runOnly: ['img-alt-present']` (a plain array — a convention used by another engine) is **silently ignored**; the engine runs every rule instead. This is the single most common integration mistake — see [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md).
|
|
22
|
+
|
|
23
|
+
| Field | Type | Meaning |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `includeRuleIds` | `string[]` | Only run these rule IDs (plus, for a composite ID, its child atomic rules). |
|
|
26
|
+
| `excludeRuleIds` | `string[]` | Never run these, applied *after* include. |
|
|
27
|
+
| `includeTestIds` / `excludeTestIds` | `string[]` | Same matching as above — kept as a separate field because rules are internally called "tests" (the atomic executable unit); functionally identical to `includeRuleIds`/`excludeRuleIds` today. |
|
|
28
|
+
| `tags` | `string[]` | Only run rules carrying at least one of these tags (e.g. `wcag412`, `wcag2aa`, `best-practice`). |
|
|
29
|
+
| `excludeTags` | `string[]` | Never run rules carrying any of these tags, applied after include. |
|
|
30
|
+
| `includeMode` | `'and'` \| `'or'` | When **both** an ID include and a tag include are given: `'and'` (default) requires a rule to satisfy both; `'or'` runs a rule if it satisfies either. Irrelevant if you only use one dimension. |
|
|
31
|
+
|
|
32
|
+
Rule IDs are bare (no engine prefix), e.g. `'img-alt-present'`. For backward compatibility, matching also accepts a legacy `a11ycore-`-prefixed form of the same id (`'a11ycore-img-alt-present'`).
|
|
33
|
+
|
|
34
|
+
A **legacy, tag-filter shape used by other engines** is also accepted as the whole `runOnly` value: `{ type: 'tag', values: ['wcag2a', 'wcag2aa'] }` — equivalent to `{ tags: ['wcag2a', 'wcag2aa'] }`.
|
|
35
|
+
|
|
36
|
+
### Filtering by WCAG version (2.1 vs 2.2)
|
|
37
|
+
|
|
38
|
+
Every rule and composite carries exactly one WCAG-version-origin level tag, matching the convention used by other engines: `wcag2a`/`wcag2aa`/`wcag2aaa` for a Success Criterion that's WCAG 2.0 baseline, `wcag21a`/`wcag21aa`/`wcag21aaa` for one newly introduced in WCAG 2.1 (e.g. `1.3.5` Identify Input Purpose), `wcag22a`/`wcag22aa`/`wcag22aaa` for one newly introduced in WCAG 2.2 (e.g. `2.5.8` Target Size Minimum). A rule gets **only** the tag for its SC's actual origin version — a 2.1-introduced SC is never also tagged `wcag2aa`, since it doesn't exist under a WCAG 2.0 conformance target. See `src/coverage/wcag-version-map.js` for the exact, canonical per-version SC list.
|
|
39
|
+
|
|
40
|
+
Since versions are cumulative (2.1 = 2.0 + new; 2.2 = 2.0 + 2.1 + new), select a WCAG-version conformance target by combining tag sets — the engine's OR-matching on `tags` (any one match includes the rule) does the rest:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// WCAG 2.0 AA only (excludes every 2.1/2.2-introduced SC, even at level AA):
|
|
44
|
+
{ tags: ['wcag2a', 'wcag2aa'] }
|
|
45
|
+
|
|
46
|
+
// WCAG 2.1 AA conformance (2.0 baseline + everything 2.1 added, both at A and AA):
|
|
47
|
+
{ tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] }
|
|
48
|
+
|
|
49
|
+
// WCAG 2.2 AA conformance (2.0 baseline + 2.1 additions + 2.2 additions):
|
|
50
|
+
{ tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'] }
|
|
51
|
+
|
|
52
|
+
// Just the SCs 2.2 introduced, nothing else:
|
|
53
|
+
{ tags: ['wcag22a', 'wcag22aa', 'wcag22aaa'] }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Via `engineOptions` (no `runOnly`)
|
|
57
|
+
|
|
58
|
+
Same filtering, expressed as comma-separated strings (or arrays) nested in `engineOptions`:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
runDomRulesInPage(url, null, {
|
|
62
|
+
rules: { include: 'img-alt-present, button-name-present', exclude: 'region' },
|
|
63
|
+
tags: { include: 'wcag412', exclude: 'best-practice' },
|
|
64
|
+
includeMode: 'and'
|
|
65
|
+
}, null);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`rules.include`/`.exclude`, `tags.include`/`.exclude`, `tests.include`/`.exclude` (alias of `rules`), and top-level `includeMode` mirror the `runOnly` fields above exactly. Comma-separated strings are trimmed, de-duplicated, and empty tokens dropped automatically.
|
|
69
|
+
|
|
70
|
+
## `engineOptions` — the rest
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const engineOptions = {
|
|
74
|
+
locale: 'en', // default 'en'; falls back to 'en' per-string if a key is missing in the requested locale
|
|
75
|
+
includeShadowDom: true, // default true — opt OUT with `false` to skip open shadow roots
|
|
76
|
+
excludeSelectors: ['#cookie-banner', '.third-party-widget'], // array or comma-separated string
|
|
77
|
+
timestamp: '2026-07-20T12:00:00Z', // optional — engine has no built-in clock, see OUTPUT_SCHEMA.md
|
|
78
|
+
perfStats: false, // default false — internal timing counters, debug-only shape
|
|
79
|
+
profileRules: false, // default false — per-rule timing breakdown inside perfStats
|
|
80
|
+
|
|
81
|
+
contrast: {
|
|
82
|
+
mode: 'strictConformance', // 'strictConformance' (default) | 'auditorAssist'
|
|
83
|
+
rootCanvasFallback: '#ffffff' // background assumed when the true root background isn't computable
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
policyContract: 'a11y', // 'a11y' (default) | 'generic' | inline contract object — see POLICY.md
|
|
87
|
+
policy: { // optional overrides on top of policyContract
|
|
88
|
+
coerceManualFailToCantTell: true
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
output: {
|
|
92
|
+
includeSelector: true, // set false to suppress auto-filled selectors (narrow effect — see note)
|
|
93
|
+
includeHtml: true
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
rules: {
|
|
97
|
+
'some-rule-id': { /* per-rule config, currently unused — see note */ }
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
probes: { /* optional host-supplied evidence, see note */ },
|
|
101
|
+
|
|
102
|
+
customRules: [ /* runtime-registered rules, see "Custom rules" below */ ],
|
|
103
|
+
|
|
104
|
+
// Only read by runa11yCoreAcrossFrames -- see INTEGRATION.md's "Cross-frame
|
|
105
|
+
// scanning" section. Ignored by runDomRulesInPage/runa11yCoreInPage.
|
|
106
|
+
pingWaitTime: 500, // ms to wait for a child frame to answer a ping before treating it as unreachable
|
|
107
|
+
frameWaitTime: 60000 // ms to wait for a child frame's full scan result before timing out
|
|
108
|
+
};
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
| Option | Meaning |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `locale` | Any string; resolution is per-string with graceful fallback (requested locale → `en` → the rule's literal English fallback text), so a partially-translated locale never produces missing text. See [`I18N.md`](./I18N.md) for current locale coverage. |
|
|
114
|
+
| `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). |
|
|
115
|
+
| `excludeSelectors` | Elements matching any of these selectors (and their descendants) are skipped entirely — useful for cookie banners, third-party embeds, or known-noisy widgets you don't control. |
|
|
116
|
+
| `timestamp` | Passed straight through to the result's top-level `timestamp` field; the engine does not generate one itself (deterministic-by-design). |
|
|
117
|
+
| `contrast.mode` | `strictConformance` (default): contrast rules stay silent (`notApplicable`/skip) whenever the true rendered background isn't confidently computable, to protect against false `fail`s. `auditorAssist`: trades some of that safety margin for more findings, intended for a human auditor who will double-check flagged cases, not for unattended CI gating. |
|
|
118
|
+
| `contrast.rootCanvasFallback` | The assumed page background color when it's not computable at all — only matters in `auditorAssist` mode. |
|
|
119
|
+
| `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`. |
|
|
120
|
+
| `output.includeSelector` / `.includeHtml` | Only affects the small number of rules (currently 4 of 123) that rely on the engine's automatic selector/HTML fill-in rather than building their own — most rules set `selector`/`html` themselves inside `runInPage` and are unaffected by this option. Not a reliable way to strip selectors/HTML from all output. |
|
|
121
|
+
| `rules[ruleId]` | Passed through to that rule as `ctx.config`. The plumbing exists end-to-end, but **no shipped rule currently reads `ctx.config`** — this is infrastructure for future per-rule configurability, not a lever that changes any of today's 123 rules' behavior. |
|
|
122
|
+
| `probes` | An optional, JSON-safe evidence object your host application can supply (depth- and size-capped by the engine before rules see it, via `ctx.inputs.probes`) — for future rules that might accept externally-supplied signals (e.g. real layout measurements a static DOM scan can't compute itself). Not consumed by any current rule. |
|
|
123
|
+
| `perfStats` / `profileRules` | Debug-only. `perfStats: true` returns internal counters on the result's `perfStats` field; `profileRules: true` additionally adds a per-rule timing breakdown. Shape is not part of the stable output contract — don't build on it. |
|
|
124
|
+
| `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`. |
|
|
125
|
+
|
|
126
|
+
## `customRules` — runtime-registered rules (equivalent to the rule/check registration pattern used by other engines)
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
130
|
+
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:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
{
|
|
134
|
+
id: 'my-org-custom-rule', // required
|
|
135
|
+
meta: { title, description, tags, defaultSeverity, defaultConfidence, /* same fields as a rule module's meta */ },
|
|
136
|
+
runInPage(ctx) { /* same ctx shape and same return contract as any built-in rule */ },
|
|
137
|
+
applicability(ctx) { return true; }, // optional, same contract as a built-in rule's applicability
|
|
138
|
+
data: { /* optional, JSON-serializable */ }
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- `runInPage`/`applicability` may be a **real function** or a **function-source string** (i.e. `fn.toString()`). Pass a real function when `engineOptions` never leaves the current JS realm (plain Node/jsdom use). Pass a string when it does — e.g. a Playwright `page.evaluate(runa11yCoreInPage, { engineOptions })` call, where `engineOptions` crosses a JSON/structured-clone boundary that cannot carry a live `Function` reference but can carry a string. The engine reconstructs a string via `new Function`, the same mechanism `scripts/build-core.js` already uses to embed every built-in rule's source into the in-page runner.
|
|
143
|
+
- `meta` gets identical defaulting/validation to a build-time rule (via the same `normalizeRuleMeta` used for the other 125 rules) — omit anything you don't need; `severity` defaults to `moderate`, `confidence` to `medium`, `type` to `automatic`, etc.
|
|
144
|
+
- A custom rule whose `id` collides with a built-in one **overrides it for that scan** (matches the override semantics used by other engines' configuration APIs), rather than running both. Since a same-named custom rule is just as likely to be an accidental collision as a deliberate override, every collision is surfaced two ways: a `console.warn` naming the id(s), and a top-level `overriddenBuiltinIds` array on the result (empty when there's no collision) — see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md).
|
|
145
|
+
- An invalid descriptor (missing/non-string `id`, or a `runInPage` that isn't a function and isn't a reconstructable source string) is silently skipped — the rest of the scan, including every built-in rule, still runs normally. This isn't a validation gap to fix: a custom rule is arbitrary caller-supplied code, so "fail this one entry closed, don't abort the scan" is the safer default, mirroring how a *built-in* rule that throws is contained to a `cantTell` for that rule rather than crashing the run.
|
|
146
|
+
- Results appear in `checksResults` exactly like any other rule's, including automatic `selector`/`html`/`structuralPath` fill-in for `fail`/`cantTell` occurrences that only attach `{ __node }` (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md)).
|
|
147
|
+
|
|
148
|
+
## `contextSelector` (2nd runner argument, not an `engineOptions` field)
|
|
149
|
+
|
|
150
|
+
A CSS selector (or array of selectors) scoping the scan to one or more subtrees, resolved via `document.querySelectorAll` (all matches, not just the first), falling back to `document.documentElement`/`document.body` if nothing matches. Pass `null` to scan the whole document.
|
|
151
|
+
|
|
152
|
+
- **A single string** may itself be a comma-separated selector list (ordinary CSS union semantics) — `'#a, #b'` scans both `#a` and `#b`.
|
|
153
|
+
- **An array of strings** scans the union of every selector's matches — `['#a', '.card']` behaves the same as `'#a, .card'`; the array form exists for callers building the list programmatically. Other engines' equivalent is calling an `.include()` method multiple times.
|
|
154
|
+
- Overlapping/nested regions are deduped automatically — an element reachable from more than one matched root is only ever reported once, not once per region.
|
|
155
|
+
- This changed from single-match (`querySelector`) to all-matches (`querySelectorAll`) semantics for the plain-string form too (2026-07-22) — a selector matching several elements previously scanned only the first, silently dropping the rest. If you relied on that first-match-only behavior, pin to a selector that only ever matches one element (e.g. an `#id`).
|
package/docs/I18N.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Internationalization (i18n) guide
|
|
2
|
+
|
|
3
|
+
Every rule's title/description, and every occurrence's summary/hint, is localized via a key-based dictionary lookup — not hard-coded per language.
|
|
4
|
+
|
|
5
|
+
## Current locale coverage
|
|
6
|
+
|
|
7
|
+
| Locale | File | Keys | Coverage vs. English |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| `en` (English) | `src/i18n/en.js` | 590 | 100% (the canonical/fallback set) |
|
|
10
|
+
| `fr` (French) | `src/i18n/fr.js` | 287 | ~49% — **partial**, not every string is translated yet |
|
|
11
|
+
|
|
12
|
+
That `fr` number is real and worth being honest about: about half of the engine's translatable strings don't have a French entry yet. That's not a bug — see the fallback behavior below, which means a missing `fr` key never produces broken or missing text, just an English string in the middle of otherwise-French output.
|
|
13
|
+
|
|
14
|
+
## Selecting a locale
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
runDomRulesInPage(url, null, { locale: 'fr' }, null);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Default is `'en'` if omitted. Any string is accepted — an unrecognized locale (e.g. `'de'`, not yet built) behaves exactly like a locale that's 0% translated: every string falls through to English (see below), not an error.
|
|
21
|
+
|
|
22
|
+
## Fallback behavior (per-string, not per-locale)
|
|
23
|
+
|
|
24
|
+
Resolution happens independently for *every individual string*, not once for the whole scan:
|
|
25
|
+
|
|
26
|
+
1. Look up the key in the requested locale's dictionary.
|
|
27
|
+
2. If missing, fall back to the English (`en`) dictionary.
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
This means requesting `locale: 'fr'` today gives you a real mix — translated strings where `fr.js` has the key, English strings where it doesn't — never a blank, `undefined`, or thrown error. See `t()` in `scripts/build-core.js` if you need the exact implementation.
|
|
31
|
+
|
|
32
|
+
## Where keys are used
|
|
33
|
+
|
|
34
|
+
Two independent key namespaces, both resolved the same way:
|
|
35
|
+
|
|
36
|
+
- **Rule-level**: `meta.i18n.titleKey` / `meta.i18n.descriptionKey` — resolve a rule's `title`/`description` on every `checksResults[]` entry.
|
|
37
|
+
- **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).
|
|
38
|
+
|
|
39
|
+
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`.
|
|
40
|
+
|
|
41
|
+
## Contributing a translation
|
|
42
|
+
|
|
43
|
+
1. Open `src/i18n/en.js` — it's the canonical key list (590 entries, one `module.exports` object of `key: string`).
|
|
44
|
+
2. Add matching keys to `src/i18n/<locale>.js` (create the file if the locale doesn't exist yet — follow `fr.js`'s structure exactly: `'use strict'; module.exports = { ...keys };`).
|
|
45
|
+
3. 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` does today. Ship what you have.
|
|
46
|
+
4. Keep `{{placeholder}}` tokens in translated strings exactly as they appear in the English source — they're substituted verbatim regardless of locale (e.g. `{{element}}`, `{{role}}`).
|
|
47
|
+
5. Run `npm run build && npm test` — there's no locale-completeness test today (a partial locale is valid, not a failure), but this confirms nothing else broke.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Integration guide
|
|
2
|
+
|
|
3
|
+
surea11y/core is a library, not a CLI or a service — you call it from your own Node script, test suite, or browser-automation code. This page covers the real ways to run it, plus how to wire it into CI.
|
|
4
|
+
|
|
5
|
+
## Which runner function to use
|
|
6
|
+
|
|
7
|
+
The first two take the same four arguments — `(pageUrl, contextSelector, engineOptions, runOnly)` — and return the same result shape (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md)). They differ only in *where* they can run:
|
|
8
|
+
|
|
9
|
+
| Function | Use when | Why |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `runDomRulesInPage` | Calling directly in the same Node process where you `require('@surea11y/core')` | Normal function call — references the module's own closures (`CHECK_DEFS`, rule implementations, etc.) directly. |
|
|
12
|
+
| `runa11yCoreInPage` | Handing the function itself to a *different* JS realm — most commonly Puppeteer/Playwright's `page.evaluate` | Fully self-contained: its entire body (rule catalog, implementations, shared helpers) is inlined, so `fn.toString()` + re-evaluating that source in a browser tab (which has no access to your Node module scope at all) still works. Verified by `tests/runa11yCoreInPage-serialization.test.js`, which literally does this — reconstructs the function from source in a separate VM realm and runs it. |
|
|
13
|
+
| `runa11yCoreAcrossFrames` / `a11yCoreEnableFrameResponder` | Same context as `runa11yCoreInPage` (browser extension / content script / bundled widget code — no automation driver), when you also need to reach into `<iframe>`s | See "Cross-frame scanning" below — a separate, async pair of functions, not a variant of the other two. |
|
|
14
|
+
|
|
15
|
+
Both of the first two need a real `document`/`window` to already exist in whatever context they run in — neither runner creates one. That's the actual fork in the two patterns below.
|
|
16
|
+
|
|
17
|
+
## Pattern 1 — jsdom in Node (no real browser)
|
|
18
|
+
|
|
19
|
+
Good for: server-rendered HTML, static files, CI without a browser dependency, unit-testing components.
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const { JSDOM } = require('jsdom');
|
|
23
|
+
const { runDomRulesInPage } = require('@surea11y/core');
|
|
24
|
+
|
|
25
|
+
const html = '<!doctype html><html><body><img src="logo.png"></body></html>';
|
|
26
|
+
const dom = new JSDOM(html, { url: 'https://example.com/', pretendToBeVisual: true });
|
|
27
|
+
|
|
28
|
+
// The runners read `document`/`window` as ambient globals, not as parameters.
|
|
29
|
+
global.window = dom.window;
|
|
30
|
+
global.document = dom.window.document;
|
|
31
|
+
|
|
32
|
+
const result = runDomRulesInPage('https://example.com/', null, {}, null);
|
|
33
|
+
|
|
34
|
+
console.log(result.checksResults.filter((r) => r.outcome === 'fail'));
|
|
35
|
+
|
|
36
|
+
dom.window.close();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`pretendToBeVisual: true` matters — it's what makes jsdom compute *something* for `getComputedStyle` (needed by the contrast rules and anything checking computed layout properties). Note jsdom has no real CSS layout engine — see [`LIMITATIONS.md`](./LIMITATIONS.md) for what that rules out entirely (e.g. `target-size-minimum` needs real `getBoundingClientRect()` and will report `notApplicable` under plain jsdom).
|
|
40
|
+
|
|
41
|
+
## Pattern 2 — a real browser via Puppeteer or Playwright
|
|
42
|
+
|
|
43
|
+
Good for: fully-rendered pages (client-side-rendered apps, real CSS layout/paint), testing your actual production site, anything Pattern 1's jsdom limitations rule out.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// Puppeteer
|
|
47
|
+
const puppeteer = require('puppeteer');
|
|
48
|
+
const { runa11yCoreInPage } = require('@surea11y/core');
|
|
49
|
+
|
|
50
|
+
const browser = await puppeteer.launch();
|
|
51
|
+
const page = await browser.newPage();
|
|
52
|
+
await page.goto('https://example.com/');
|
|
53
|
+
|
|
54
|
+
const result = await page.evaluate(
|
|
55
|
+
runa11yCoreInPage, // page.evaluate serializes this function and runs it inside the page
|
|
56
|
+
'https://example.com/',
|
|
57
|
+
null, // contextSelector
|
|
58
|
+
{}, // engineOptions
|
|
59
|
+
null // runOnly
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
console.log(result.checksResults.filter((r) => r.outcome === 'fail'));
|
|
63
|
+
await browser.close();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
// Playwright — NOT the same shape as Puppeteer. Playwright's page.evaluate(fn, arg)
|
|
68
|
+
// only ever accepts ONE arg value; page.evaluate(fn, a, b, c, d) throws
|
|
69
|
+
// "Too many arguments. If you need to pass more than 1 argument to the
|
|
70
|
+
// function wrap them in an object." (confirmed against a real Playwright
|
|
71
|
+
// page — this is not a theoretical distinction). Since runa11yCoreInPage
|
|
72
|
+
// itself takes 4 positional arguments, wrap it in a single-arg function
|
|
73
|
+
// that destructures one options object, embedding runa11yCoreInPage's own
|
|
74
|
+
// source via .toString() so the wrapper is still fully self-contained once
|
|
75
|
+
// serialized into the page (the same technique used by this project's
|
|
76
|
+
// internal live-DOM comparison tooling, maintained outside this repo).
|
|
77
|
+
const wrapperSource = `(args) => {
|
|
78
|
+
const runa11yCoreInPage = ${runa11yCoreInPage.toString()};
|
|
79
|
+
return runa11yCoreInPage(args.url, args.contextSelector, args.engineOptions, args.runOnly);
|
|
80
|
+
}`;
|
|
81
|
+
// eslint-disable-next-line no-eval
|
|
82
|
+
const wrapperFn = eval(wrapperSource);
|
|
83
|
+
|
|
84
|
+
const result = await page.evaluate(wrapperFn, {
|
|
85
|
+
url,
|
|
86
|
+
contextSelector: null,
|
|
87
|
+
engineOptions: {},
|
|
88
|
+
runOnly: null
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This is the only pattern that gives every rule real computed layout, so it's the one to reach for if you need `target-size-minimum` or any other geometry-dependent check to actually run instead of reporting `notApplicable`.
|
|
93
|
+
|
|
94
|
+
## Scoping a scan to part of the page
|
|
95
|
+
|
|
96
|
+
Pass a CSS selector as the 2nd argument (`contextSelector`) to scan one subtree instead of the whole document — e.g. `runDomRulesInPage(url, '#app', {}, null)` to skip a surrounding CMS chrome you don't control. Pass an array of selectors (or a single comma-separated selector string) to scan multiple, possibly disjoint regions in one run — e.g. `runDomRulesInPage(url, ['#header', '#main'], {}, null)`. See [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md) for the full `contextSelector` reference and for `excludeSelectors`, the complementary "skip specific elements anywhere" option.
|
|
97
|
+
|
|
98
|
+
## CI: gating a build on the result
|
|
99
|
+
|
|
100
|
+
The engine returns data, not a verdict — deciding what fails your build is up to you. The straightforward gate is "any `fail` outcome, in the atomic results, fails the build":
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const failures = result.checksResults.filter((r) => r.outcome === 'fail');
|
|
104
|
+
if (failures.length > 0) {
|
|
105
|
+
console.error(`${failures.length} accessibility rule(s) failed:`);
|
|
106
|
+
for (const f of failures) {
|
|
107
|
+
console.error(` ${f.ruleId}: ${f.occurrences.length} occurrence(s)`);
|
|
108
|
+
}
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Notes for CI specifically:
|
|
114
|
+
- `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.
|
|
115
|
+
- There's no built-in baseline/allowlist mechanism yet for "only fail on *new* violations" — if you need that today, diff `checksResults` against a saved prior run yourself.
|
|
116
|
+
- 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.
|
|
117
|
+
|
|
118
|
+
## Browser extension context
|
|
119
|
+
|
|
120
|
+
`runa11yCoreInPage` is also the right function for a content-script/DevTools-panel context — inject it the same way you'd inject any content script, call it directly (no serialization step needed there since it's already running in the page's own realm), and it has no dependency on the extension's own execution environment beyond a standard DOM.
|
|
121
|
+
|
|
122
|
+
### Cross-frame scanning (including cross-origin)
|
|
123
|
+
|
|
124
|
+
`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 — the same real scenario other engines' own cross-frame messaging protocols exist for.
|
|
125
|
+
|
|
126
|
+
`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, exactly like other engines' equivalent mechanisms are.
|
|
127
|
+
|
|
128
|
+
**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** — this mirrors the same real limitation other engines have for non-cooperating frames; it is not a gap `surea11y` closes that they don't have either.
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
// Inside the embedded/child page (e.g. a widget's own bundle), once, at load:
|
|
132
|
+
const { a11yCoreEnableFrameResponder } = require('@surea11y/core');
|
|
133
|
+
a11yCoreEnableFrameResponder(); // opts this frame in to being scanned from above
|
|
134
|
+
|
|
135
|
+
// Inside the parent page:
|
|
136
|
+
const { runa11yCoreAcrossFrames } = require('@surea11y/core');
|
|
137
|
+
const result = await runa11yCoreAcrossFrames(null, null, {}, null);
|
|
138
|
+
|
|
139
|
+
console.log(result.topFrame.checksResults.filter((r) => r.outcome === 'fail')); // this document's own findings
|
|
140
|
+
for (const frame of result.frames) {
|
|
141
|
+
if (frame.error) continue; // unreachable -- no cooperating responder, or it timed out
|
|
142
|
+
console.log(frame.topFrame.checksResults.filter((r) => r.outcome === 'fail')); // that frame's findings
|
|
143
|
+
// frame.frames holds ITS OWN nested children, recursively -- a tree, not a flat list
|
|
144
|
+
// (unlike Playwright's .frames(true), which can flatten since page.frames() already
|
|
145
|
+
// gives every frame regardless of nesting depth; a postMessage relay can't know about
|
|
146
|
+
// a grandchild without asking through its own child first).
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A few things worth knowing:
|
|
151
|
+
- **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).
|
|
152
|
+
- **`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, matching the defaults other engines use for the equivalent options.
|
|
153
|
+
- **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.
|
|
154
|
+
- **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.
|
|
155
|
+
- **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.
|
|
156
|
+
- **No origin/identity check on the sender** beyond the message's own namespaced envelope — matching the same permissiveness other engines take here. 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.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Known limitations
|
|
2
|
+
|
|
3
|
+
Stated plainly and upfront, not left for you to discover. Every item here is a deliberate, reasoned decision, not an oversight — but "deliberate" doesn't mean "unimportant to know before you rely on this tool."
|
|
4
|
+
|
|
5
|
+
## Structural — this architecture cannot do these at all
|
|
6
|
+
|
|
7
|
+
surea11y is a **static DOM scan**: it reads the DOM tree and computed styles at one instant, with no ability to simulate user interaction, wait for async state changes, or measure real layout at arbitrary viewport sizes. These aren't missing rules — no rule implementation, however clever, can close them without a fundamentally different architecture (real browser automation driving actual keyboard/pointer events over time):
|
|
8
|
+
|
|
9
|
+
- **Keyboard-trap detection** (WCAG 2.1.2) — requires simulating actual focus/keydown sequences and observing whether focus can escape. No static markup signal exists for this.
|
|
10
|
+
- **Reflow / clipping at zoom** (WCAG 1.4.10) — requires real layout measurement (`clientWidth`/`scrollWidth`) at a simulated 320px-equivalent viewport. Unlike some CSS-declaration-based heuristics elsewhere in this engine, there is no static markup proxy for "does content get clipped at 400% zoom" at all.
|
|
11
|
+
- **Dynamic/post-interaction state** — anything that only exists after a click, hover, or async data load (a modal's contents, a dropdown's options, form-validation error messages) is invisible to a scan of the page's *current* DOM. If your framework renders it eagerly (even off-screen/`hidden`), it's scannable; if it only exists after interaction, it isn't, unless you drive that interaction yourself before scanning (e.g. click the button, *then* scan).
|
|
12
|
+
|
|
13
|
+
## Environment-dependent — depends on how you run it
|
|
14
|
+
|
|
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
|
+
- **`<dialog>` and other elements hidden by the default UA stylesheet** (no `open` attribute, `display: none` by spec) are skipped by most tools' visibility-aware checks, including other established engines — but surea11y's static-markup-validity checks (like ARIA ID-reference validity) still evaluate them, since a markup defect is still a defect even before the dialog opens. This is a deliberate surea11y choice that can occasionally make it *more* thorough than other engines on hidden content, not a bug — found and confirmed during internal cross-engine verification.
|
|
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/core scan <url>`) specifically fetches static HTML only, with no JS execution — see [`CLI.md`](./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
|
+
|
|
19
|
+
## Deliberately not attempted — judgment calls, not automatable safely
|
|
20
|
+
|
|
21
|
+
These have no comparably safe heuristic at this engine's confidence bar (`fail` must stay reserved for deterministic, high-confidence 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):
|
|
22
|
+
|
|
23
|
+
- **"Is this heading/label text meaningful?"** — real headings and labels are enormously varied and legitimately short ("FAQ," "Name," "Overview" are all fine). Unlike link text (where a small, well-established "always bad" phrase list exists — see `link-name-quality`), there's no equivalent safe list here.
|
|
24
|
+
- **"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.
|
|
25
|
+
- **Fine-grained time-based-media sub-checks** (WCAG 1.2.x has ~8 distinct ACT-rule-level cases beyond what's built) — audio/video content itself is fundamentally unverifiable from static markup; the two broadest, safest cases are covered (`media-alternative-transcript-evidence`, `video-caption`), the narrower ones are not, by design.
|
|
26
|
+
- **Images-of-text content analysis** (WCAG 1.4.5/1.4.9) — would need OCR-equivalent image understanding; out of scope for a static-markup engine.
|
|
27
|
+
- **Motion-actuation controls** (WCAG 2.5.4) — niche, low real-world incidence; not prioritized, not structurally impossible.
|
|
28
|
+
|
|
29
|
+
## What this means in practice
|
|
30
|
+
|
|
31
|
+
None of the above is unique to surea11y — every static-analysis accessibility tool (other established engines included) shares the structural limitations, and most share the judgment-call ones too. The reason to state it explicitly here: a `pass` from this engine (or any automated tool) is never a substitute for the manual review WCAG itself requires for the criteria above. See [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for exactly what a `pass`/composite `pass` does and doesn't claim.
|