@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,237 @@
|
|
|
1
|
+
# Output schema reference
|
|
2
|
+
|
|
3
|
+
This is the exact shape of the object returned by `runDomRulesInPage(...)` / `runa11yCoreInPage(...)` (see [`INTEGRATION.md`](./INTEGRATION.md) for which one to call). Every example on this page is real output from the current engine (`schemaVersion: "1.0.0"`), not hand-written. `runa11yCoreAcrossFrames` returns a different, recursive shape wrapping this one — see [Cross-frame result](#cross-frame-result-runa11ycoreacrossframes) below.
|
|
4
|
+
|
|
5
|
+
- [Top-level result](#top-level-result)
|
|
6
|
+
- [Cross-frame result (`runa11yCoreAcrossFrames`)](#cross-frame-result-runa11ycoreacrossframes)
|
|
7
|
+
- [A check result (`checksResults[i]`)](#a-check-result-checksresultsi)
|
|
8
|
+
- [An occurrence (`occurrences[i]`)](#an-occurrence-occurrencesi)
|
|
9
|
+
- [A composite result (`rulesResults[i]`)](#a-composite-result-rulesresultsi)
|
|
10
|
+
- [Outcome values](#outcome-values)
|
|
11
|
+
- [Severity and confidence values](#severity-and-confidence-values)
|
|
12
|
+
- [Worked example](#worked-example)
|
|
13
|
+
|
|
14
|
+
## Top-level result
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
{
|
|
18
|
+
engine: { tag: string, schemaVersion: string },
|
|
19
|
+
url: string | null,
|
|
20
|
+
title: string | null,
|
|
21
|
+
timestamp: string | null,
|
|
22
|
+
perfStats: object | null,
|
|
23
|
+
contextSelector: string | string[] | null,
|
|
24
|
+
checksResults: CheckResult[],
|
|
25
|
+
rulesResults: CompositeResult[],
|
|
26
|
+
overriddenBuiltinIds: string[]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Field | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `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
|
+
| `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. |
|
|
34
|
+
| `url` | The `pageUrl` argument you passed in, or `document.location.href` if you passed `null`/omitted it, or `null` if neither is available. |
|
|
35
|
+
| `title` | `document.title` at scan time, or `null`. |
|
|
36
|
+
| `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. |
|
|
37
|
+
| `perfStats` | `null` unless `engineOptions.perfStats: true`. Internal timing/counters — shape not covered by this document, treat as debug-only. |
|
|
38
|
+
| `contextSelector` | The (trimmed) `contextSelector` argument you passed — a string, an array of strings (multi-region scanning, see [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md)), or `null` if none/empty. |
|
|
39
|
+
| `checksResults` | One entry per **atomic rule** that ran (every rule not filtered out by `runOnly` — see [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md)). **Every loaded rule produces an entry, even ones that outcome `notApplicable`** — this is not a "violations only" list. |
|
|
40
|
+
| `rulesResults` | One entry per **composite (WCAG-SC rollup) rule** that ran — see [Composite result](#a-composite-result-rulesresultsi) and [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md). Empty array if no composite matched the current `runOnly`/tag filter. |
|
|
41
|
+
| `overriddenBuiltinIds` | Rule ids where an `engineOptions.customRules` entry shared its `id` with a built-in rule, so the custom implementation replaced the built-in one for this scan (see [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md)). Always an array; empty when no collision occurred. Also logged via `console.warn` at scan time, since a same-named custom rule is as likely to be an accidental collision as a deliberate override. |
|
|
42
|
+
|
|
43
|
+
## Cross-frame result (`runa11yCoreAcrossFrames`)
|
|
44
|
+
|
|
45
|
+
`runa11yCoreAcrossFrames` (see [`INTEGRATION.md`](./INTEGRATION.md#cross-frame-scanning-including-cross-origin)) returns a different, recursive shape instead of a plain top-level result:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
{
|
|
49
|
+
topFrame: <the normal top-level result shape above>,
|
|
50
|
+
frames: Array<
|
|
51
|
+
| { url: string | null, topFrame: <top-level result>, frames: [...same shape, recursively] }
|
|
52
|
+
| { url: string | null, error: string }
|
|
53
|
+
>
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `topFrame` is exactly the [top-level result](#top-level-result) shape, for the frame the function was called in.
|
|
58
|
+
- `frames` has one entry per direct child `<iframe>`/`<frame>` in the scanned scope. A reachable child (one that called `a11yCoreEnableFrameResponder()`) contributes its own complete `{ url, topFrame, frames }` — including *its own* nested `frames`, recursively, since a further-nested grandchild is only reachable through its immediate parent. An unreachable child (the common case for most third-party embeds — no cooperating responder, or it timed out) contributes `{ url, error }` instead, and does not abort the rest of the scan.
|
|
59
|
+
- This is a **tree, not a flat list** — a deliberate difference from the `surea11y-playwright` binding's `.frames(true)`, which *can* flatten because Playwright's `page.frames()` already gives every frame regardless of nesting depth; a `postMessage` relay has no such global view, so nesting is expressed structurally instead.
|
|
60
|
+
|
|
61
|
+
## A check result (`checksResults[i]`)
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
{
|
|
65
|
+
ruleId: string,
|
|
66
|
+
outcome: "pass" | "fail" | "cantTell" | "notApplicable",
|
|
67
|
+
outcomeNormalized: "pass" | "fail" | "cantTell" | "inapplicable",
|
|
68
|
+
severity: "minor" | "moderate" | "serious" | "critical",
|
|
69
|
+
confidence: "high" | "medium" | "low",
|
|
70
|
+
type: "automatic" | "manual",
|
|
71
|
+
occurrences: Occurrence[],
|
|
72
|
+
title: string,
|
|
73
|
+
description: string,
|
|
74
|
+
i18n: { titleKey: string, descriptionKey: string } | null,
|
|
75
|
+
meta: {
|
|
76
|
+
ruleId: string,
|
|
77
|
+
ruleInterfaceVersion: string,
|
|
78
|
+
ruleVersion: string,
|
|
79
|
+
normative: boolean,
|
|
80
|
+
atomic: boolean,
|
|
81
|
+
category: "perceivable" | "operable" | "understandable" | "robust" | null,
|
|
82
|
+
normativeMappings: Array<{ standard: string, version: string, requirement: string, title: string, conformanceLevel: string }>,
|
|
83
|
+
standard: string | null,
|
|
84
|
+
applicability: string,
|
|
85
|
+
expectation: string,
|
|
86
|
+
references: string[],
|
|
87
|
+
requirements: object | null,
|
|
88
|
+
mappings: object | null
|
|
89
|
+
},
|
|
90
|
+
engineOptions: object, // the resolved engineOptions this rule actually ran under
|
|
91
|
+
schemaVersion: string,
|
|
92
|
+
error?: string // present only if the rule threw — see below
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Notes:
|
|
97
|
+
|
|
98
|
+
- **`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
|
+
- **`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 (other engines call these "Best Practices"; this engine calls them advisory `type: "manual"` rules). See [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for how these roll up.
|
|
101
|
+
- **`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
|
+
- **`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
|
+
|
|
104
|
+
## An occurrence (`occurrences[i]`)
|
|
105
|
+
|
|
106
|
+
Only present when `outcome` is `fail` or `cantTell` (a `pass`/`notApplicable` result has `occurrences: []` — this engine does not enumerate the elements it passed, only the ones it flagged; see the note in `docs/RULE_AUTHORING.md` on why "silence" from a `pass` rule is not the same as an enumerated list of passing elements).
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
{
|
|
110
|
+
selector: string,
|
|
111
|
+
html: string,
|
|
112
|
+
structuralPath: number[] | null,
|
|
113
|
+
summary: string,
|
|
114
|
+
hint: string,
|
|
115
|
+
i18n: { summaryKey: string, hintKey: string, params: object } | null,
|
|
116
|
+
data: {
|
|
117
|
+
visibilityFilter?: { targetSet: string, accEligible: boolean | null, reasons: string[] },
|
|
118
|
+
details?: object // rule-specific, non-normative — see below
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Field | Meaning |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `selector` | A best-effort CSS selector built to resolve back to the flagged element (see `helpers.buildSelector` in `RULE_AUTHORING.md`). Not guaranteed unique in adversarial DOM shapes, but the engine actively verifies it resolves to the reported element before using it. |
|
|
126
|
+
| `html` | An outer-HTML snippet of the flagged element — use this as your primary "which element" signal when `includeShadowDom: true` (selectors don't pierce shadow boundaries). |
|
|
127
|
+
| `structuralPath` | The flagged element's sibling-index path from `documentElement` down to it (e.g. `[1, 0, 2]`) — `[]` if the element *is* `documentElement`, `null` if it couldn't be determined. A more robust element-identity mechanism than `selector` alone: it survives DOM changes a selector string wouldn't (an id/class rename, for instance), at the cost of not being usable as an actual CSS selector. Computed from the element reference when the rule kept one, otherwise by re-resolving `selector` against the document (same caveat as `selector` itself: a non-unique selector could resolve to a different element than intended). |
|
|
128
|
+
| `summary` | Human-readable, already localized ("This button has no accessible name."). |
|
|
129
|
+
| `hint` | Human-readable remediation guidance, already localized. |
|
|
130
|
+
| `i18n` | The raw translation keys behind `summary`/`hint`, if you want to re-render them in a different locale yourself without re-running the scan. `null` if the occurrence didn't use key-based i18n. |
|
|
131
|
+
| `data.visibilityFilter` | Present on most occurrences: why the engine considered this element eligible for accessibility-tree evaluation (or not). `reasons` is a list of machine-readable exclusion codes when `accEligible: false`. |
|
|
132
|
+
| `data.details` | Rule-specific structured data (e.g. `reasonCode`, computed metrics, resolved references) — **non-normative**: useful for building richer UI or debugging, but never changes what `outcome`/`severity` mean. Shape varies per rule; treat as best-effort extra context, not a stable contract. |
|
|
133
|
+
|
|
134
|
+
## A composite result (`rulesResults[i]`)
|
|
135
|
+
|
|
136
|
+
Composites roll multiple atomic rules up to one WCAG Success Criterion (e.g. `wcag-1.1.1-non-text-content` rolls up 22 atomic rules). Shape is the same envelope as a check result, with composite-specific `data.details`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
{
|
|
140
|
+
ruleId: string, // e.g. "wcag-1.1.1-non-text-content"
|
|
141
|
+
outcome: "pass" | "fail" | "cantTell" | "notApplicable",
|
|
142
|
+
severity, confidence, type, title, description, meta, engineOptions, schemaVersion, // same as a check result
|
|
143
|
+
occurrences: [], // always empty — composites are rollups, not element-level findings
|
|
144
|
+
data: {
|
|
145
|
+
details: {
|
|
146
|
+
reasonCode: string, // e.g. "composite.rollup.fail.anyFail"
|
|
147
|
+
checksIds: string[], // every atomic ruleId this composite rolls up
|
|
148
|
+
contributors: Array<{ testId: string, outcome: string, severity: string | null }>,
|
|
149
|
+
metrics: { failCount, cantTellCount, notApplicableCount, passCount, missingCount }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Rollup precedence (deterministic, in this order): **any contributor `fail` → composite `fail`**; else **any `cantTell` (or a contributor rule that didn't run at all, `missingCount > 0`) → composite `cantTell`**; else **all contributors `notApplicable` → composite `notApplicable`**; else **`pass`**. See [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for what this means for an overall conformance claim.
|
|
156
|
+
|
|
157
|
+
## Outcome values
|
|
158
|
+
|
|
159
|
+
| Outcome | Meaning | Can appear on `type: "manual"`? |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `fail` | Deterministic, high-confidence, normative violation — no heuristics, no guessing. | No (coerced to `cantTell`) |
|
|
162
|
+
| `pass` | The rule's applicable target(s) exist and none were flagged. | Yes |
|
|
163
|
+
| `cantTell` | Requires human judgment — either genuinely ambiguous, or a `manual` rule's advisory finding. | Yes |
|
|
164
|
+
| `notApplicable` | The rule found no elements it applies to on this page/scope. | Yes |
|
|
165
|
+
|
|
166
|
+
`fail` is intentionally the narrowest, highest-bar outcome in this engine: reserved for deterministic, normative violations; chasing rule coverage must never dilute this.
|
|
167
|
+
|
|
168
|
+
## Severity and confidence values
|
|
169
|
+
|
|
170
|
+
- `severity`: `minor` < `moderate` < `serious` < `critical` — the rule author's assessment of user impact, independent of `confidence`.
|
|
171
|
+
- `confidence`: `low` < `medium` < `high` — how certain the engine is that a `fail`/`cantTell` verdict is correct. Both are informational metadata for prioritization; neither changes `outcome`'s meaning.
|
|
172
|
+
|
|
173
|
+
## Worked example
|
|
174
|
+
|
|
175
|
+
Scanning `<img src="logo.png">` (no `alt`) and `<button></button>` (no accessible name), scoped to just those two rules via `runOnly: { includeRuleIds: [...] }` (see [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md) — this is **not** a bare array):
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
const result = runDomRulesInPage(
|
|
179
|
+
'https://example.test/',
|
|
180
|
+
null,
|
|
181
|
+
{},
|
|
182
|
+
{ includeRuleIds: ['img-alt-present', 'button-name-present'] }
|
|
183
|
+
);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"engine": { "tag": "a11ycore", "schemaVersion": "1.0.0" },
|
|
189
|
+
"url": "https://example.test/",
|
|
190
|
+
"title": "Example",
|
|
191
|
+
"timestamp": null,
|
|
192
|
+
"perfStats": null,
|
|
193
|
+
"contextSelector": null,
|
|
194
|
+
"checksResults": [
|
|
195
|
+
{
|
|
196
|
+
"ruleId": "button-name-present",
|
|
197
|
+
"outcome": "fail",
|
|
198
|
+
"severity": "serious",
|
|
199
|
+
"confidence": "high",
|
|
200
|
+
"type": "automatic",
|
|
201
|
+
"occurrences": [
|
|
202
|
+
{
|
|
203
|
+
"selector": "html > body > button",
|
|
204
|
+
"html": "<button></button>",
|
|
205
|
+
"structuralPath": [1, 0],
|
|
206
|
+
"summary": "This button has no accessible name.",
|
|
207
|
+
"hint": "Provide visible button text or a programmatic accessible-name mechanism (for example aria-label) so assistive technologies can identify the button.",
|
|
208
|
+
"data": {
|
|
209
|
+
"visibilityFilter": { "eligible": true, "reasons": [], "targetSet": "acc", "accEligible": true },
|
|
210
|
+
"details": { "reasonCode": "name_missing" }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"ruleId": "img-alt-present",
|
|
217
|
+
"outcome": "fail",
|
|
218
|
+
"severity": "serious",
|
|
219
|
+
"confidence": "high",
|
|
220
|
+
"type": "automatic",
|
|
221
|
+
"occurrences": [
|
|
222
|
+
{
|
|
223
|
+
"selector": "html > body > img",
|
|
224
|
+
"html": "<img src=\"logo.png\">",
|
|
225
|
+
"structuralPath": [1, 1],
|
|
226
|
+
"summary": "Missing alt attribute on <img>.",
|
|
227
|
+
"hint": "Add an alt attribute (use alt=\"\" only for decorative images)."
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
],
|
|
232
|
+
"rulesResults": [],
|
|
233
|
+
"overriddenBuiltinIds": []
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
(Trimmed for readability — the real result also includes `title`/`description`/`i18n`/`meta`/`engineOptions`/`schemaVersion` on every entry, per the full shape above. `rulesResults` is empty here because `runOnly.includeRuleIds` scoped the scan to two atomic rules and no composite's own ID was included.)
|
package/docs/POLICY.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Policy & contracts guide
|
|
2
|
+
|
|
3
|
+
A policy controls two things, independent of any individual rule's logic: **which outcome/confidence values are allowed to reach the result at all**, and **whether a `manual` rule's would-be `fail` gets coerced to `cantTell`**. It never changes what a rule decides — only how that decision is allowed to be represented.
|
|
4
|
+
|
|
5
|
+
## The two built-in contracts
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
{
|
|
9
|
+
a11y: {
|
|
10
|
+
id: 'a11y',
|
|
11
|
+
allowedOutcomes: ['fail', 'pass', 'cantTell', 'notApplicable'],
|
|
12
|
+
allowedConfidence: ['high', 'medium', 'low'],
|
|
13
|
+
coerceManualFailToCantTell: true // ← the only difference
|
|
14
|
+
},
|
|
15
|
+
generic: {
|
|
16
|
+
id: 'generic',
|
|
17
|
+
allowedOutcomes: ['fail', 'pass', 'cantTell', 'notApplicable'],
|
|
18
|
+
allowedConfidence: ['high', 'medium', 'low'],
|
|
19
|
+
coerceManualFailToCantTell: false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
(Source of truth: `src/policy/contracts.js`.)
|
|
25
|
+
|
|
26
|
+
- **`a11y`** (the default): enforces the engine's core non-negotiable — a `type: 'manual'` rule (advisory/judgment-required, see [`RULE_CATALOG.md`](./RULE_CATALOG.md)) can never produce a `fail`. If a manual rule's own logic decides `fail`, the policy coerces it to `cantTell` and appends a note to the result's `error` field. Use this contract for anything where a `fail` result carries weight — CI gating, compliance reporting, anywhere someone might treat `fail` as "definitely broken."
|
|
27
|
+
- **`generic`**: identical outcome/confidence vocabulary, but does **not** coerce manual `fail`s. Only meaningful if you've deliberately reconfigured a manual rule to be more assertive than its default and want that respected — not a general-purpose "looser" mode.
|
|
28
|
+
|
|
29
|
+
## Selecting a contract
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
runDomRulesInPage(url, null, { policyContract: 'a11y' }, null); // default if omitted
|
|
33
|
+
runDomRulesInPage(url, null, { policyContract: 'generic' }, null);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or supply an inline contract object instead of a name:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
runDomRulesInPage(url, null, {
|
|
40
|
+
policyContract: {
|
|
41
|
+
id: 'my-custom-policy',
|
|
42
|
+
allowedOutcomes: ['fail', 'pass', 'cantTell', 'notApplicable'],
|
|
43
|
+
allowedConfidence: ['high', 'medium'], // drop 'low' — anything low-confidence becomes cantTell
|
|
44
|
+
coerceManualFailToCantTell: true
|
|
45
|
+
}
|
|
46
|
+
}, null);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Any field you omit from an inline contract object falls back to the `a11y` contract's value for that field — you're overriding, not replacing wholesale.
|
|
50
|
+
|
|
51
|
+
## Fine-grained overrides
|
|
52
|
+
|
|
53
|
+
`engineOptions.policy` overrides individual fields on top of whichever contract you selected, without defining a whole new contract:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
runDomRulesInPage(url, null, {
|
|
57
|
+
policyContract: 'a11y',
|
|
58
|
+
policy: { coerceManualFailToCantTell: false } // keep everything else about 'a11y', just flip this one flag
|
|
59
|
+
}, null);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## What happens when a value isn't allowed
|
|
63
|
+
|
|
64
|
+
- **`outcome` not in `allowedOutcomes`**: silently coerced to `cantTell`. (In practice this only matters for custom contracts that narrow the outcome list — the two built-in contracts allow all four values.)
|
|
65
|
+
- **`confidence` not in `allowedConfidence`**: silently replaced with the rule's own `defaultConfidence`.
|
|
66
|
+
|
|
67
|
+
Neither of these ever throws — policy resolution is designed to always produce a valid result, per the engine's "safe-by-default" principle.
|
|
68
|
+
|
|
69
|
+
## Why this exists as a separate layer
|
|
70
|
+
|
|
71
|
+
Keeping outcome-integrity rules (like "manual rules can't fail") in a policy layer — rather than hard-coded into every rule, or worse, left to each rule author's discretion — means the guarantee holds even if a rule's own logic has a bug, and means different consumers can have different appetites for risk (a CI gate vs. an internal audit dashboard) without forking the rule set itself. This protects the engine's core guarantee: `fail` must always mean "deterministic, high-confidence, normative violation," full stop.
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# RULE_AUTHORING.md — a11yCore DOM Rule Authoring (Canonical, repo-derived)
|
|
2
|
+
|
|
3
|
+
This guide is derived from the **actual rule modules, helpers, build pipeline, and tests** in this codebase.
|
|
4
|
+
Follow it literally when adding or modifying rules.
|
|
5
|
+
|
|
6
|
+
> Key principle: **Atomic + deterministic + standards-traceable**.
|
|
7
|
+
> One rule = one normative decision.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1) Where rules run (critical mental model)
|
|
12
|
+
|
|
13
|
+
Rules are bundled into the generated core and executed inside the **page/DOM context**.
|
|
14
|
+
|
|
15
|
+
- `runInPage(ctx)` is **serialized** and evaluated later from its source text.
|
|
16
|
+
- Therefore it must be **self-contained** (no outer-scope references).
|
|
17
|
+
|
|
18
|
+
### 1.1 Forbidden inside `runInPage`
|
|
19
|
+
❌ Don’t reference anything defined outside the function body, including:
|
|
20
|
+
- `id`
|
|
21
|
+
- `meta`
|
|
22
|
+
- imported modules
|
|
23
|
+
- closure variables
|
|
24
|
+
|
|
25
|
+
This is a known, **recurring** footgun (“meta is not defined” incident).
|
|
26
|
+
|
|
27
|
+
⚠️ **Why this is dangerous, not just annoying: the build does NOT fail.** `runInPage` is serialized via `fn.toString()` and re-evaluated as source text later, in the page context — `build-core.js` never parses that source for free variables, and `npm run build`/`npm test`'s own tests only verify serialization round-trips correctly, not that every identifier resolves. The break only surfaces when the rule actually *runs*: the reference throws a `ReferenceError` inside `runInPage`, the runner's own `try/catch` (`src/core/dom-runner.js`) catches it silently, and the rule's result becomes `{ outcome: 'cantTell', occurrences: [], error: '<name> is not defined' }` — a normal-looking result, not a crash. A rule broken this way can sit unnoticed indefinitely unless something specifically asserts its `outcome`/`error`, which is why every rule's fixture-coverage test (§11) matters: it's often the *only* thing that would catch this.
|
|
28
|
+
|
|
29
|
+
**If you add a module-scope `const`/helper function to a rule file, move it inside `runInPage` itself** (or route the value through `ctx.rule`/`ctx.helpers` if it must be engine-provided) — do not leave it at module scope and reference it from inside `runInPage`, even though nothing will complain until you actually run the rule and check its `error` field.
|
|
30
|
+
|
|
31
|
+
✅ Use `ctx.rule.*` instead:
|
|
32
|
+
- `ctx.rule.ruleId`
|
|
33
|
+
- `ctx.rule.defaultSeverity`
|
|
34
|
+
- `ctx.rule.defaultConfidence`
|
|
35
|
+
- `ctx.rule.type`
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2) Rule module contract (exact)
|
|
40
|
+
|
|
41
|
+
Each rule file is a CommonJS module exporting exactly:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
'use strict';
|
|
45
|
+
|
|
46
|
+
const id = 'some-rule-id';
|
|
47
|
+
|
|
48
|
+
const meta = { /* see Meta Contract */ };
|
|
49
|
+
|
|
50
|
+
function runInPage(ctx) { /* see Runtime Contract */ }
|
|
51
|
+
|
|
52
|
+
module.exports = { id, meta, runInPage };
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
No other exports.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 3) Rule ID conventions (repo reality)
|
|
60
|
+
|
|
61
|
+
IDs are kebab-case, bare (no engine prefix).
|
|
62
|
+
|
|
63
|
+
Common pattern used in this ruleset:
|
|
64
|
+
```
|
|
65
|
+
<target>-<topic>-<intent>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Examples observed:
|
|
69
|
+
- `img-alt-present`
|
|
70
|
+
- `img-alt-quality`
|
|
71
|
+
- `img-alt-decorative`
|
|
72
|
+
- `canvas-text-alternative-present`
|
|
73
|
+
- `video-poster-text-alternative-present`
|
|
74
|
+
|
|
75
|
+
**Manual vs automatic is NOT encoded in the id** in this repo; it is encoded by `meta.type`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 4) Meta Contract (all keys used by current rules)
|
|
80
|
+
|
|
81
|
+
Every rule defines a `meta` object. In the rule set you uploaded, the union of meta keys is:
|
|
82
|
+
|
|
83
|
+
### 4.1 Required top-level keys
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
const meta = {
|
|
87
|
+
title: '…',
|
|
88
|
+
description: '…',
|
|
89
|
+
|
|
90
|
+
i18n: {
|
|
91
|
+
titleKey: '…',
|
|
92
|
+
descriptionKey: '…'
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
helpUrl: null, // or URL string
|
|
96
|
+
|
|
97
|
+
tags: [ '…' ],
|
|
98
|
+
wcagSc: [ '1.1.1' ],
|
|
99
|
+
|
|
100
|
+
normativeMappings: [
|
|
101
|
+
{
|
|
102
|
+
standard: 'WCAG',
|
|
103
|
+
version: '2.2',
|
|
104
|
+
requirement: '1.1.1',
|
|
105
|
+
title: 'Non-text Content',
|
|
106
|
+
conformanceLevel: 'A'
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
|
|
110
|
+
defaultSeverity: 'minor' | 'moderate' | 'serious' | 'critical',
|
|
111
|
+
category: 'perceivable' | 'operable' | 'understandable' | 'robust',
|
|
112
|
+
type: 'automatic' | 'manual',
|
|
113
|
+
defaultConfidence: 'high' | 'medium' | 'low',
|
|
114
|
+
|
|
115
|
+
coverage: {
|
|
116
|
+
facetsBySc: {
|
|
117
|
+
'1.1.1': ['facet-a', 'facet-b']
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4.2 Notes on specific meta keys
|
|
124
|
+
|
|
125
|
+
#### `meta.i18n`
|
|
126
|
+
This repo uses **key-based i18n**:
|
|
127
|
+
- `titleKey`, `descriptionKey` are dictionary keys.
|
|
128
|
+
- `title` and `description` remain as **English fallbacks**.
|
|
129
|
+
|
|
130
|
+
The build/runtime resolves i18n by:
|
|
131
|
+
1) looking up the requested locale dictionary,
|
|
132
|
+
2) falling back to `en` if missing,
|
|
133
|
+
3) falling back to the literal `title`/`description` strings if still missing.
|
|
134
|
+
|
|
135
|
+
#### `meta.tags`
|
|
136
|
+
Tags are used for grouping/filtering. Typical tag families in this ruleset include:
|
|
137
|
+
- WCAG tagging: `wcag2a`, `wcag111`
|
|
138
|
+
- domain: `nontext`, `images`, plus element-specific tags
|
|
139
|
+
- nature: `atomic`, plus `automatic` or `manual`
|
|
140
|
+
|
|
141
|
+
#### `meta.coverage.facetsBySc`
|
|
142
|
+
This is the repo’s explicit **coverage model** for an SC.
|
|
143
|
+
Each atomic rule declares which “facet(s)” of an SC it covers.
|
|
144
|
+
|
|
145
|
+
Keep facet naming consistent across a family.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 5) i18n in occurrences (repo reality)
|
|
150
|
+
|
|
151
|
+
Occurrences also support i18n via keys + params.
|
|
152
|
+
|
|
153
|
+
### 5.1 Occurrence i18n shape
|
|
154
|
+
|
|
155
|
+
Every occurrence may include:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
i18n: {
|
|
159
|
+
summaryKey: '…',
|
|
160
|
+
hintKey: '…',
|
|
161
|
+
params: { /* string substitutions */ }
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
At normalization time, the engine:
|
|
166
|
+
- ensures `summary`, `hint`, and `html` are strings,
|
|
167
|
+
- ensures `i18n` is either a normalized object or `null`,
|
|
168
|
+
- resolves `summary` and `hint` using i18n keys (with locale → `en` fallback → literal fallback).
|
|
169
|
+
|
|
170
|
+
### 5.2 Param interpolation
|
|
171
|
+
|
|
172
|
+
Translation strings use `{{paramName}}` placeholders.
|
|
173
|
+
`params` is shallow-copied and passed into interpolation.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 6) Helpers contract used by rules (ctx.helpers)
|
|
178
|
+
|
|
179
|
+
Rules use helpers returned by `createDomHelpers()`.
|
|
180
|
+
|
|
181
|
+
Helpers observed in this repo include:
|
|
182
|
+
- `queryAll`, `queryAllDeep`, `queryAllSmart`
|
|
183
|
+
- `getOuterHtmlSnippet`
|
|
184
|
+
- `buildSimpleSelector`, `buildSelector`
|
|
185
|
+
- `isAccTreeEligible`, `getEligibilityInfo`
|
|
186
|
+
- `resolveIdRefs`, `getTextFromIdRefs`
|
|
187
|
+
- `getAccessibleNameInfo`, `getAccessibleDescriptionInfo`
|
|
188
|
+
- `getTextAlternativeInfo`
|
|
189
|
+
- `getRoleInfo`, `getFocusableInfo`
|
|
190
|
+
|
|
191
|
+
### 6.1 Shadow DOM scanning
|
|
192
|
+
|
|
193
|
+
Rules that need to work with open Shadow DOM should prefer:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
const nodes = helpers.queryAllSmart
|
|
197
|
+
? helpers.queryAllSmart('img')
|
|
198
|
+
: helpers.queryAll('img');
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Shadow traversal is opt-in via engine option:
|
|
202
|
+
```js
|
|
203
|
+
engineOptions: { includeShadowDom: true }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 6.2 Reporting note for Shadow DOM
|
|
207
|
+
|
|
208
|
+
Selectors do not pierce shadow boundaries, so a `selector` may not uniquely locate nodes in Shadow DOM.
|
|
209
|
+
Therefore: **always include `html` in occurrences**.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 7) Eligibility logging (required in this ruleset)
|
|
214
|
+
|
|
215
|
+
This repo requires rules to attach an eligibility/visibility trace in each occurrence:
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
data: {
|
|
219
|
+
visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This is consistent across your uploaded rule family.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 8) `runInPage(ctx)` runtime contract (repo reality)
|
|
228
|
+
|
|
229
|
+
### 8.1 Expected return shape
|
|
230
|
+
|
|
231
|
+
The rule must return:
|
|
232
|
+
- `ruleId` (must be `rule.ruleId`)
|
|
233
|
+
- `outcome`: `"pass" | "fail" | "cantTell" | "notApplicable"`
|
|
234
|
+
- `severity`: string
|
|
235
|
+
- `occurrences`: array
|
|
236
|
+
|
|
237
|
+
Examples:
|
|
238
|
+
|
|
239
|
+
### 8.2 Outcome conventions used by these rules
|
|
240
|
+
|
|
241
|
+
Automatic:
|
|
242
|
+
- `notApplicable` if no applicable targets
|
|
243
|
+
- `pass` if applicable targets exist and no occurrences
|
|
244
|
+
- `fail` if occurrences exist
|
|
245
|
+
|
|
246
|
+
Manual:
|
|
247
|
+
- `notApplicable` if no applicable targets
|
|
248
|
+
- `cantTell` if at least one target requires review
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 9) Occurrence object shape (repo reality)
|
|
253
|
+
|
|
254
|
+
Typical pattern:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
occurrences.push({
|
|
258
|
+
selector,
|
|
259
|
+
html,
|
|
260
|
+
summary: '…',
|
|
261
|
+
hint: '…',
|
|
262
|
+
i18n: { summaryKey: '…', hintKey: '…', params: { element: 'img' } },
|
|
263
|
+
data: { visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] } }
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Observed properties:
|
|
268
|
+
- `selector` (or sometimes `selectorStr`)
|
|
269
|
+
- `html`
|
|
270
|
+
- `summary`
|
|
271
|
+
- `hint`
|
|
272
|
+
- `i18n` (`summaryKey`, `hintKey`, `params`)
|
|
273
|
+
- `data` (includes `visibilityFilter`)
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## 10) Structured doc comment block
|
|
278
|
+
|
|
279
|
+
Keep the structured header comment (`@rule`, `@atomic`, `@summary`, `@standard`, `@sc`, `@applicability`, `@expectation`).
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 11) Scenario fixture + fixture-coverage test (required for every rule)
|
|
284
|
+
|
|
285
|
+
Every rule — automatic or manual, no exceptions — needs a standalone, loadable HTML
|
|
286
|
+
scenario page in addition to its inline unit tests. This is not optional polish: the
|
|
287
|
+
project's test fixtures are meant to be usable directly by external tooling (loaded and
|
|
288
|
+
exercised as real pages), not just embedded as strings inside `.test.js` files.
|
|
289
|
+
|
|
290
|
+
### 11.1 The fixture file
|
|
291
|
+
|
|
292
|
+
- Path: `tests/fixtures/<rule-slug>-all-scenarios.html`, where `<rule-slug>` is the rule
|
|
293
|
+
id itself (e.g. `tab-name-present` → `tab-name-present-all-scenarios.html`).
|
|
294
|
+
- Structure: a real HTML page (`<!doctype html>`, `<title>`, minimal inline `<style>`)
|
|
295
|
+
containing numbered scenario blocks, each:
|
|
296
|
+
```html
|
|
297
|
+
<div class="case" id="case_NN">
|
|
298
|
+
<div class="case-title">NN — PASS: role=tab, visible text content</div>
|
|
299
|
+
<div role="tab" tabindex="0" id="<slug>_case_NN">Apple</div>
|
|
300
|
+
</div>
|
|
301
|
+
```
|
|
302
|
+
- The `.case-title` text MUST start with `NN — MARKER:` where `MARKER` is one of
|
|
303
|
+
`PASS`, `FAIL`, `CANTTELL`, or `NEUTRAL`/`INELIGIBLE` (the fixture-index generator,
|
|
304
|
+
§11.3, parses this to count scenarios per outcome — see
|
|
305
|
+
`scripts/generate-fixture-index.js`'s `parseFixtureCases`).
|
|
306
|
+
- The actual test target gets its own stable id of the form `<slug>_case_NN` (short,
|
|
307
|
+
memorable abbreviation of the rule name — see existing fixtures for precedent, e.g.
|
|
308
|
+
`tab_case_01`, `binctl_case_01`).
|
|
309
|
+
- Group related cases under `<h2>` sections (e.g. "A. Named (eligible)", "B. Unnamed
|
|
310
|
+
(eligible, FAIL)", "C. Ineligible (excluded from accessibility tree, skipped)").
|
|
311
|
+
- Cover every branch the rule's own logic distinguishes: pass, fail (each distinct
|
|
312
|
+
`reasonCode`), notApplicable/skipped, and — for manual rules — cantTell.
|
|
313
|
+
|
|
314
|
+
### 11.2 Known, acceptable exceptions to "one fixture, many cases"
|
|
315
|
+
|
|
316
|
+
A few rule shapes genuinely cannot express every branch as a single static page. When
|
|
317
|
+
you hit one of these, still create the fixture (covering whatever branches ARE
|
|
318
|
+
expressible statically) and add an explicit `<p class="note">` in the fixture, plus a
|
|
319
|
+
comment in the `.test.js` fixture-coverage test, stating which branch is NOT covered and
|
|
320
|
+
why:
|
|
321
|
+
|
|
322
|
+
- **Whole-document checks** (e.g. `aria-hidden-body`, `page-title-present`,
|
|
323
|
+
`meta-viewport-zoom-enabled`, `bypass-blocks-present`): the property being checked
|
|
324
|
+
exists once per page (one `<body>`, one `<title>`, one viewport meta), so only one
|
|
325
|
+
outcome is demonstrable per fixture file. Pick the most illustrative FAIL case; note
|
|
326
|
+
that PASS/other branches are covered by the rule's inline unit tests instead of
|
|
327
|
+
minting near-duplicate fixture files.
|
|
328
|
+
- **Runtime-mutation-only branches** (e.g. `iframe-focusable-content`'s FAIL branch,
|
|
329
|
+
which requires mutating `iframe.contentDocument` after parse — jsdom does not
|
|
330
|
+
populate `srcdoc` synchronously): cover every branch that IS expressible statically;
|
|
331
|
+
leave the rest to the existing programmatic test.
|
|
332
|
+
- **Rules with no branching logic at all** (e.g. `manual-review`, which always returns
|
|
333
|
+
`cantTell` regardless of page content): a single trivial case is fine, purely for
|
|
334
|
+
index completeness — say so in the fixture's note.
|
|
335
|
+
|
|
336
|
+
Do not force a false "PASS" demonstration or fabricate a scenario that doesn't actually
|
|
337
|
+
exercise the code path it claims to.
|
|
338
|
+
|
|
339
|
+
### 11.3 The fixture-coverage test
|
|
340
|
+
|
|
341
|
+
Add one test to the rule's existing `tests/engine-checks/**/<rule>.test.js` (do not
|
|
342
|
+
create a separate file):
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
const fs = require('node:fs');
|
|
346
|
+
const path = require('node:path');
|
|
347
|
+
|
|
348
|
+
test(`${RULE_ID}: fixture coverage (tests/fixtures/<rule-slug>-all-scenarios.html)`, () => {
|
|
349
|
+
const fixturePath = path.join(__dirname, '../..', 'fixtures', '<rule-slug>-all-scenarios.html');
|
|
350
|
+
const fixtureHtml = fs.readFileSync(fixturePath, 'utf8');
|
|
351
|
+
const result = runa11yCoreOnHtml(fixtureHtml, { runOnly: [RULE_ID] });
|
|
352
|
+
|
|
353
|
+
const rule = assertRule(result, RULE_ID, 'fail', { minOccurrences: N, maxOccurrences: N });
|
|
354
|
+
// assert the exact expected-fail ids (and, if useful, expected-no-occurrence ids)
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The file MUST declare `const RULE_ID = '...'` near the top (the fixture-index
|
|
359
|
+
generator discovers a rule's test file and fixture by scanning for that constant —
|
|
360
|
+
tests using only inline string literals won't be picked up; see
|
|
361
|
+
`tests/engine-checks/manual-review.test.js` for the fix applied when this was missed).
|
|
362
|
+
|
|
363
|
+
### 11.4 Keeping the index current
|
|
364
|
+
|
|
365
|
+
After adding or changing any fixture, regenerate the index:
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
npm run fixtures:index
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
This writes `tests/fixtures/INDEX.md` (human-readable) and `tests/fixtures/index.json`
|
|
372
|
+
(machine-readable — every rule, its fixture path, and parsed pass/fail/cantTell case
|
|
373
|
+
counts, for external tooling to enumerate and load fixtures directly). Commit both
|
|
374
|
+
alongside the fixture and test changes. A rule shipped without its fixture is treated
|
|
375
|
+
the same as a rule shipped without tests — not done.
|