@surea11y/core 1.4.1 → 1.6.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 +212 -128
- package/README.md +46 -9
- package/docs/ACT_RULE_MAPPING.md +243 -0
- package/docs/API_STABILITY.md +4 -4
- package/docs/BINDING_AUTHORS_GUIDE.md +3 -3
- package/docs/DESIGN_CHALLENGES.md +301 -0
- package/docs/ENGINE_OPTIONS.md +30 -14
- package/docs/I18N.md +176 -20
- package/docs/INTEGRATION.md +29 -7
- package/docs/LIMITATIONS.md +6 -4
- package/docs/OUTPUT_SCHEMA.md +13 -3
- package/docs/REPORT.md +3 -1
- package/docs/RULE_AUTHORING.md +104 -23
- package/docs/RULE_CATALOG.md +1878 -169
- package/docs/RULE_TAXONOMY.md +2 -2
- package/docs/TROUBLESHOOTING.md +4 -4
- package/docs/WCAG_CONFORMANCE.md +25 -9
- package/package.json +8 -7
- package/src/baseline.js +3 -3
- package/src/checks/automatic/area-alt-present.js +2 -2
- package/src/checks/automatic/aria-allowed-attr.js +95 -40
- package/src/checks/automatic/aria-allowed-role.js +16 -18
- package/src/checks/automatic/aria-braille-equivalent.js +19 -21
- package/src/checks/automatic/aria-conditional-attr.js +22 -24
- package/src/checks/automatic/aria-deprecated-role.js +63 -50
- package/src/checks/automatic/aria-hidden-body.js +4 -11
- package/src/checks/automatic/aria-hidden-focus.js +104 -23
- package/src/checks/automatic/aria-prohibited-attr.js +71 -72
- package/src/checks/automatic/aria-prohibited-children.js +154 -61
- package/src/checks/automatic/aria-required-attr.js +74 -29
- package/src/checks/automatic/aria-required-children.js +38 -34
- package/src/checks/automatic/aria-required-parent.js +78 -29
- package/src/checks/automatic/aria-role-name-present.js +36 -22
- package/src/checks/automatic/aria-roles-valid.js +37 -23
- package/src/checks/automatic/aria-valid-attr-value.js +33 -33
- package/src/checks/automatic/aria-valid-attr.js +15 -18
- package/src/checks/automatic/autocomplete-valid.js +17 -19
- package/src/checks/automatic/avoid-inline-spacing.js +14 -16
- package/src/checks/automatic/binary-control-name-present.js +46 -26
- package/src/checks/automatic/button-name-present.js +115 -34
- package/src/checks/automatic/combobox-name-present.js +40 -22
- package/src/checks/automatic/contrast-computable.js +32 -0
- package/src/checks/automatic/contrast-enhanced.js +21 -1
- package/src/checks/automatic/contrast-minimum.js +21 -1
- package/src/checks/automatic/css-orientation-lock.js +118 -41
- package/src/checks/automatic/definition-list-children-valid.js +25 -29
- package/src/checks/automatic/deprecated-elements-not-used.js +15 -17
- package/src/checks/automatic/dialog-name-present.js +36 -20
- package/src/checks/automatic/dlitem-parent-valid.js +15 -17
- package/src/checks/automatic/duplicate-id-aria.js +50 -40
- package/src/checks/automatic/duplicate-id.js +198 -0
- package/src/checks/automatic/embed-text-alternative-present.js +2 -2
- package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
- package/src/checks/automatic/form-control-single-label.js +39 -41
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -9
- package/src/checks/automatic/iframe-focusable-content.js +92 -38
- package/src/checks/automatic/iframe-name-present.js +53 -21
- package/src/checks/automatic/iframe-title-unique.js +19 -24
- package/src/checks/automatic/img-alt-present.js +12 -4
- package/src/checks/automatic/label-in-name.js +198 -49
- package/src/checks/automatic/link-in-text-block.js +29 -31
- package/src/checks/automatic/link-name-present.js +47 -31
- package/src/checks/automatic/list-children-valid.js +21 -23
- package/src/checks/automatic/listbox-name-present.js +42 -24
- package/src/checks/automatic/listitem-parent-valid.js +18 -21
- package/src/checks/automatic/menuitem-name-present.js +36 -20
- package/src/checks/automatic/meta-refresh-no-exceptions.js +39 -31
- package/src/checks/automatic/meta-refresh-timing-absent.js +29 -25
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +14 -17
- package/src/checks/automatic/meter-name-present.js +38 -21
- package/src/checks/automatic/nested-interactive-controls-absent.js +22 -24
- package/src/checks/automatic/option-name-present.js +39 -22
- package/src/checks/automatic/page-title-present.js +21 -3
- package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
- package/src/checks/automatic/progressbar-name-present.js +41 -24
- package/src/checks/automatic/role-img-alt-present.js +64 -16
- package/src/checks/automatic/searchbox-name-present.js +46 -24
- package/src/checks/automatic/server-side-image-map-absent.js +16 -19
- package/src/checks/automatic/slider-name-present.js +42 -23
- package/src/checks/automatic/spinbutton-name-present.js +46 -24
- package/src/checks/automatic/summary-name-present.js +34 -20
- package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
- package/src/checks/automatic/svg-text-alternative-present.js +13 -10
- package/src/checks/automatic/tab-name-present.js +37 -20
- package/src/checks/automatic/table-headers-attr-valid.js +57 -24
- package/src/checks/automatic/table-th-has-data-cells.js +76 -24
- package/src/checks/automatic/target-size-minimum.js +172 -131
- package/src/checks/automatic/td-has-header.js +20 -25
- package/src/checks/automatic/textbox-name-present.js +42 -24
- package/src/checks/automatic/tooltip-name-present.js +37 -20
- package/src/checks/automatic/treeitem-name-present.js +39 -22
- package/src/checks/automatic/valid-lang.js +107 -24
- package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
- package/src/checks/manual/accesskeys-manual.js +21 -22
- package/src/checks/manual/area-alt-decorative-manual.js +7 -0
- package/src/checks/manual/area-alt-quality-manual.js +6 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +23 -26
- package/src/checks/manual/aria-text-manual.js +4 -4
- package/src/checks/manual/bypass-blocks-present-manual.js +48 -38
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/empty-heading-manual.js +73 -28
- package/src/checks/manual/empty-table-header-manual.js +33 -36
- package/src/checks/manual/focus-order-semantics-manual.js +15 -15
- package/src/checks/manual/form-control-label-quality-manual.js +453 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +4 -11
- package/src/checks/manual/heading-order-manual.js +20 -25
- package/src/checks/manual/heading-quality-manual.js +338 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
- package/src/checks/manual/image-redundant-alt-manual.js +18 -21
- package/src/checks/manual/img-alt-decorative-manual.js +211 -52
- package/src/checks/manual/img-alt-quality-manual.js +7 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
- package/src/checks/manual/label-title-only-manual.js +19 -21
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +21 -24
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +23 -26
- package/src/checks/manual/landmark-main-is-top-level-manual.js +20 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +8 -13
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +4 -9
- package/src/checks/manual/landmark-one-main-manual.js +8 -15
- package/src/checks/manual/landmark-unique-manual.js +31 -36
- package/src/checks/manual/link-name-quality-manual.js +162 -35
- package/src/checks/manual/media-transcript-present-manual.js +2 -3
- package/src/checks/manual/meta-viewport-large-manual.js +16 -19
- package/src/checks/manual/mouse-only-event-handlers-manual.js +26 -28
- package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
- package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
- package/src/checks/manual/p-as-heading-manual.js +4 -4
- package/src/checks/manual/page-has-heading-one-manual.js +8 -15
- package/src/checks/manual/page-title-patterns-manual.js +26 -3
- package/src/checks/manual/presentation-role-conflict-manual.js +74 -46
- package/src/checks/manual/region-manual.js +32 -25
- package/src/checks/manual/scope-attr-valid-manual.js +16 -19
- package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
- package/src/checks/manual/skip-link-manual.js +44 -52
- package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
- package/src/checks/manual/tabindex-manual.js +16 -19
- package/src/checks/manual/table-duplicate-name-manual.js +16 -19
- package/src/checks/manual/table-fake-caption-manual.js +2 -2
- package/src/checks/manual/video-caption-manual.js +3 -3
- package/src/checks/manual-review.js +17 -1
- package/src/core.js +13086 -5169
- package/src/report.js +16 -2
- package/surea11y.browser.js +5665 -4219
- package/surea11y.i18n.de.js +22 -0
- package/surea11y.i18n.es.js +22 -0
- package/surea11y.i18n.fr.js +22 -0
- package/bin/surea11y-core.js +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -4,218 +4,302 @@ All notable changes to this project are documented here, in [Keep a Changelog](h
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.6.0] - 2026-08-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `duplicate-id` flags a non-empty `id` repeated within the same document or shadow tree. Tagged `wcag2a` plus `wcag22-removed` since SC 4.1.1 (Parsing) was dropped in WCAG 2.2, so exclude it from a 2.2 conformance run with `excludeTags: ['wcag22-removed']`. See `docs/ENGINE_OPTIONS.md`.
|
|
11
|
+
- `form-control-label-quality` reviews a field's visible label text: a leftover placeholder, a label reused across several fields with nothing telling them apart, or a label split between visible and hidden parts. Sits alongside `form-control-programmatic-label-quality`, which judges the labelling mechanism instead of the text.
|
|
12
|
+
- `css-focus-indicator-suppressed` flags `:focus`/`:focus-visible` rules that remove the outline with no replacement drawn anywhere else.
|
|
13
|
+
- `heading-quality` flags placeholder heading text: generic words, numbered template slots, filenames, URLs.
|
|
14
|
+
- `presentational-children-focusable-absent` catches a focusable descendant left inside a role that makes its own children presentation-only (`button`, `img`, `option`, `tab`, ...): reachable by keyboard, but with no exposed role or name.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `docs/RULE_CATALOG.md` now includes each rule's applicability and expectation text alongside its summary, generated straight from source.
|
|
18
|
+
- `link-name-quality` weighs nearby context before flagging a link as non-descriptive (an `aria-describedby` target, or the enclosing list item/table cell/paragraph's own text) rather than judging the accessible name on its own. It also catches bare file-format names like "HTML" or "PDF" for the first time.
|
|
19
|
+
- Rule summaries and hints are reworded throughout: same meaning, plainer phrasing. Anything snapshot-testing that text will see a diff.
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
- `bin/surea11y-core.js` and its `bin` entry, unused since the CLI moved to `@surea11y/cli` in 1.4.0.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- `aria-prohibited-children` no longer treats a container's *required* owned elements as the exhaustive list of children it may own. WAI-ARIA's "Required Owned Elements" says what a container must contain, not all it may contain. Using the required set as the allowed set also flagged markup the spec itself describes: a `role="separator"` between menu items, and a `role="caption"` on a `role="table"`/`role="grid"`. A second table now carries the difference; an entry needs a source, either an ARIA Required Context Role naming the container or the child role's own definition placing it there. `scripts/generate-aria-tables.js` generates the table and rejects any entry with neither (`treegrid: caption` and `rowgroup: rowheader` are both refused, with the reasoning recorded). Separators under `list`/`listbox`/`tablist`, captions under `treegrid`, and any other unsourced role still fail, and the failure message now lists the full allowed set rather than only the required roles.
|
|
26
|
+
- `aria-prohibited-children` no longer reports an item's own content as a prohibited child of the container. The walk treats a roleless wrapper as transparent, which it has to, since component markup routinely buries the real item several roleless levels down (an Angular Material card whose radio sits at `card > header > mat-radio-button > div > div > input[type=radio]`). But that transparency ran in both directions, so any role-bearing element anywhere in an item's subtree registered as an owned child of the container too far up, e.g. a `role="separator"` dividing two columns inside a radio card was reported as a prohibited child of the enclosing `role="radiogroup"`, six levels up. A roleless wrapper that holds a required item is now treated as an item wrapper: only the items it holds count as the container's owned children, and the rest of that subtree belongs to the item. This touches all twelve container roles in `REQUIRED_OWNED_ROLES`. Unchanged: `role="none"`/`"presentation"` wrappers, which really are removed from the accessibility tree with their children promoted, so a disallowed role inside one is still an owned child; a roleless wrapper holding no item at all, which is interposed content and still reported; and any genuinely stray direct child of the container.
|
|
27
|
+
- `aria-role-name-present` no longer fails `tablist`, `toolbar`, `menu`, `menubar` and `scrollbar` for having no accessible name. Its role list was a hand-written allowlist of roles WAI-ARIA lets an author name, which is the wrong predicate: the spec records *Name From* and *Accessible Name Required* separately, and those five are `nameRequired: false`. A single tab widget under a visible heading, a mainstream and usable pattern, was reported as a `serious` WCAG 4.1.2 Level A failure at `high` confidence, with no ACT counterpart validating the scope. The rule now evaluates only the roles ARIA actually requires a name for (`grid`, `meter`, `progressbar`, `radiogroup`, `tree`), so its Level A mapping is true of every finding it emits; the five dropped roles are out of applicability rather than passing. The set is generated from `aria-query` by `scripts/generate-aria-tables.js` and re-derived in the rule's tests, so it can't drift back to a hand-picked list. `meter`/`progressbar` stay despite their dedicated rules, which map to SC 1.1.1: this rule is their only 4.1.2 coverage. Open questions (a `cantTell`-tier alternative that was considered and rejected, and the five name-*required* roles still uncovered: `table`, `tabpanel`, `treegrid`, `application`, `marquee`) are tracked in `docs/DESIGN_CHALLENGES.md`.
|
|
28
|
+
- `isAccTreeEligible` ignored `content-visibility: hidden`, so content the browser skips entirely (no accessibility tree, no focus, no find-in-page) stayed in every rule's candidate list and could be reported as a failure. It blocks the subtree now, like `display:none`, using the `contentVisibilityHidden` reason `isDomVisibleEligible` already reported. The element carrying the declaration is unaffected: it hides its contents, not itself, so it stays in scope.
|
|
29
|
+
- `contrast-minimum`/`contrast-enhanced` stopped requiring a contrast ratio for text made entirely of punctuation or symbols; digit-only text is still checked.
|
|
30
|
+
- `contrast-computable` treats a declared `text-shadow` as a computability blocker (`cantTell`) instead of asserting pass or fail: a strong shadow can rescue contrast that would otherwise fail.
|
|
31
|
+
- `contrast-minimum`/`contrast-enhanced` stopped reporting a failure when text's foreground color exactly matches its background; that text isn't visible in the first place.
|
|
32
|
+
- `identical-links-same-purpose` covers `[role="link"]` elements too, not just `a[href]`, and can pull a link's destination out of an `onclick` attribute when there's no real `href`.
|
|
33
|
+
- `iframe-name-present` requires an iframe to be reachable by keyboard focus before it needs a name, and stopped flagging one already marked decorative with `role="none"`/`"presentation"`.
|
|
34
|
+
- `css-orientation-lock` uses a ±5-degree tolerance for its 90/270-degree rotation check instead of demanding an exact multiple of 90, and now decomposes `matrix()`/`matrix3d()`/`rotate3d()` rotations as well.
|
|
35
|
+
- `iframe-focusable-content` resolves a `srcdoc` iframe's embedded content, and exempts iframes 2px or smaller (tracking pixels) from needing to be free of focusable content.
|
|
36
|
+
- `bypass-blocks-present`'s heading mechanism requires the heading to actually be visible, not just present in the accessibility tree.
|
|
37
|
+
- `aria-valid-attr-value` stopped failing a bare or empty ARIA attribute value, and no longer requires `aria-errormessage`'s target to exist (`aria-activedescendant` still does).
|
|
38
|
+
- `valid-lang` scopes to text that actually inherits its language from the target element, rather than any text anywhere in its subtree.
|
|
39
|
+
- `role-img-text-alternative-present` covers `role="graphics-symbol"`/`"graphics-document"` too, including nested SVG shapes, and stopped flagging elements inside `aria-hidden="true"`.
|
|
40
|
+
- `img-alt-decorative` reviews any visible `<img>`/`<canvas>`/`<svg>` excluded from the accessibility tree, not just `<img alt="">`.
|
|
41
|
+
- `label-in-name` stopped flagging visible text standing in for an icon: a single character absent from the accessible name, or text rendered through a known icon font.
|
|
42
|
+
- `aria-prohibited-children` treats `role="group"`/`"rowgroup"` wrappers as transparent for every container role, not only the ones that also accept `group`/`rowgroup` as a leaf role themselves.
|
|
43
|
+
- Native containment roles (`<li>` to `listitem`, `<option>` to `option`, the table row/cell roles) are conditional on the element's actual parent now, matching HTML-AAM. Affects `aria-required-children`, `aria-prohibited-children`, `aria-required-parent`.
|
|
44
|
+
- `aria-allowed-attr` judges elements with no ARIA role at all (`<audio>`/`<video>`, and `<div>`/`<span>` as `generic`) instead of skipping them.
|
|
45
|
+
- `table-headers-attr-valid` scopes strictly to `table`/`grid`/`treegrid`; any other explicit role takes a table out of scope entirely.
|
|
46
|
+
- `aria-required-attr` requires `aria-valuenow` on a focusable `role="separator"`.
|
|
47
|
+
- `presentation-role-conflict` stopped flagging an `<img alt="">` that carries an explicit role of its own.
|
|
48
|
+
- Ten German/Spanish/French strings for `input-image-alt-present` had lost diacritics and apostrophes; restored.
|
|
49
|
+
- 19 rules had an English `title`/`description` that disagreed between source and dictionary; corrected, and `validate-rule.js` now asserts the two match.
|
|
50
|
+
- `scripts/generate-aria-tables.js --check` could never pass: it compared unformatted output against the formatted, committed tables.
|
|
51
|
+
|
|
52
|
+
## [1.5.0] - 2026-08-16
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- `engine.locale` on the result records which dictionary a run actually used: `{ requested, resolved, reason }`, once per result. Locale fallback is graceful and per-string, so asking for a language the build doesn't carry has always produced fluent English with nothing in the output to say so. `reason` now distinguishes `ok`, `unknown-locale` (no dictionary for that code, a region subtag counts as its own locale) and `partial-dictionary` (dictionary used but missing keys). Purely additive, so `engine.schemaVersion` stays `"1.0.0"`. Documented in `docs/OUTPUT_SCHEMA.md` and `docs/API_STABILITY.md`.
|
|
56
|
+
- `npm run i18n:sync` rewrites every non-English locale file against `en.json`: adds keys that are new (seeded with the English text), drops keys `en.json` no longer has, and leaves existing translations untouched. `npm run i18n:check` performs the same comparison without writing and exits non-zero on drift. Adding a rule string previously meant editing four files by hand, with a missed one falling back to English silently.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- The standalone browser bundle carries English only, and every other locale ships beside it as `surea11y.i18n.<locale>.js`, loaded with a second `<script>` tag. The bundle drops from 1580 KB to 1300 KB and stops growing as languages are added. Asking for a locale whose side file is not loaded returns English with `engine.locale.reason` set to `dictionary-not-loaded`, so it's visible rather than silent. `require('@surea11y/core')` and every binding are unaffected and still carry all locales. `scripts/build-browser.js` now generates its own English-only core in memory instead of reading the shipped `src/core.js`.
|
|
60
|
+
- `npm run build` removes a `surea11y.i18n.<locale>.js` whose locale no longer exists. `files` matches those by glob, so a dropped language would otherwise have kept shipping.
|
|
61
|
+
- New `engineOptions.messages`, a `{ [locale]: { key: text } }` map checked before the built-in tables. It can override individual strings or supply a language the build does not carry, and it's how a locale side file reaches the in-page runner. Omitted keys fall back normally.
|
|
62
|
+
- Locale codes are matched case-insensitively, so `pt-br` and `PT-BR` both find `pt-BR.json`. Only an exact spelling matched before. A code differing from its dictionary only in case reports `ok` rather than `primary-subtag`, since no fallback happened.
|
|
63
|
+
- A locale code carrying a subtag now falls back to its base language before falling back to English: `de-DE` and `de-AT` both use `de.json`, matched case-insensitively, and an exact `de-DE.json` still wins if one exists. Previously any subtag resolved straight to English, so a browser-supplied `de-DE` produced English output while `de` produced German. `engine.locale.reason` reports `primary-subtag` for it. A translator now only needs `pt.json` to serve every Portuguese variant.
|
|
64
|
+
- Locale sources are JSON (`src/i18n/*.json`) rather than CommonJS modules, so a translation is a data change with no executable code in the diff. The generated `src/core.js` and `surea11y.browser.js` are byte-identical across the conversion. `src/i18n/*` was already documented as internal and has never been in the published `files` allowlist.
|
|
65
|
+
- `npm run i18n:new` writes a locale file mirroring `en.json`'s key order. Its refusal to overwrite an existing locale now points at `i18n:sync`, which updates a file without discarding translations.
|
|
66
|
+
- Locale completeness is now asserted for every locale rather than for a hand-maintained list of the ones documented as complete; `i18n:sync` makes full key parity the only valid state.
|
|
67
|
+
- `aria-deprecated-role` occurrences resolve their hint through `ariaDeprecatedRole_guidance_directory`/`_generic`/`_default`, replacing `ariaDeprecatedRole_hint_fail`/`_hint_cantTell`. Reason codes are unchanged, so existing baselines keep matching.
|
|
68
|
+
- `formControl_programmaticLabelQuality_summary_cantTell` interpolates `{{method}}` in place of `{{methodLabel}}`, which held the same value once an unreachable branch was removed.
|
|
69
|
+
- `css-orientation-lock` reports a rule whose selector cannot be read through `cssOrientationLock_summary_fail_unknownSelector` instead of substituting a placeholder selector name.
|
|
70
|
+
- The HTML report's meta bar carries the resolved locale alongside the engine tag and schema version, naming the requested locale too when the two differ. A report generated in a locale the engine does not carry previously read as an ordinary English one. A result from an older engine has no `engine.locale` and gets no chip.
|
|
71
|
+
- `target-size-minimum` now reports `cantTell` instead of `fail` when an undersized inline link's only spacing conflict is another inline link in the same run of text (e.g. pipe-separated links in a `<nav>`). SC 2.5.8's inline exception ("the target is in a sentence or its size is otherwise constrained by the line-height of non-target text") plausibly covers such a link, but whether it applies isn't decidable from geometry alone. Previously the outcome flipped between `fail` and `pass` based on the wrapping element's tag: the same links failed inside a `<nav>` yet passed inside a `<p>`, because the strict inline-text exception (`isInlineTextExceptionTarget`) requires a text-block container (`p`, `li`, `dd`, ...). The strict exception is unchanged; the new middle tier is scoped by a shared `isInlineLinkTarget` helper (link-like, rendered inline/inline-*, with visible text) and only downgrades when both the target and its conflicting neighbor match, so an inline link crowding a `<button>` or a block-displayed nav link still fails. New `cantTell` locale strings (`targetSizeMinimum_summary_cantTell_inlineLinkRun`/`_hint_`) in all four locales; occurrences carry reason code `undersized-inline-link-run`.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- `docs/TROUBLESHOOTING.md` still described two shipped locales and warned that key parity had to be maintained by hand. There are four, and `i18n:sync` with its build check is what removed that risk.
|
|
75
|
+
- The README brand tag rendered twice on npm, once per theme. It relied on `#gh-dark-mode-only` and `#gh-light-mode-only`, fragments only GitHub acts on; every other renderer drew both images. A `<picture>` with a `prefers-color-scheme` source picks one variant on GitHub and npm alike, with absolute paths since the tarball ships `docs/**/*.md` but not the SVGs beside them.
|
|
76
|
+
- A `fail` resting only on elements the engine could not walk to the root of is reported as `cantTell`. Ancestor walks stop after 200 steps so a malformed tree cannot hang a scan, but past that depth the walk cannot show an element is exposed, and three rules were asserting violations on content nested inside `aria-hidden`, which is the one outcome this engine must not produce. An occurrence alongside one the walk did reach still keeps the `fail`, since a single confirmed element justifies it. Counted as `ancestorsIncludingSelf.truncated` in `perfStats`. This only reaches rules that report their element through `helpers.reportOccurrence`; the rest keep the old behaviour until they're migrated.
|
|
77
|
+
- `region` dominated the runtime of any page it fired on, taking roughly four minutes on a thousand unplaced elements and scaling cubically from there. It hand-built its occurrence objects, and an occurrence that arrives without its element makes the engine re-find one with `document.querySelector` to build `structuralPath`, a document-wide query per occurrence. It reports the element now: the same page takes under a second, with identical occurrences. A new `perfStats` counter, `structuralPath.selectorFallback`, makes the expensive path visible; `docs/RULE_AUTHORING.md` §4.3 documents it as a performance contract, and `tests/structural-path-fallback.test.js` ratchets the count of rules still building occurrences by hand so it can only shrink. Every rule now reports its element: `button-name-present` on 500 unnamed buttons went from 36.9s to 0.3s. Output is byte-identical across all 130 fixtures throughout.
|
|
78
|
+
- `contrast-minimum`, `contrast-enhanced` and `contrast-computable` never examined text inside an open shadow root. Their text collection used a `TreeWalker`, which stops at a shadow boundary, and a `querySelectorAll` for value-bearing inputs, which doesn't cross one either, so low-contrast text in a web component was reported as `notApplicable` rather than checked. Every open shadow root beneath a scan root is now walked in its own right, nested roots included. `includeShadowDom: false` still skips them, closed roots remain unreachable, and text is counted once whether it's slotted or not. Background resolution already crossed the boundary and is unchanged.
|
|
79
|
+
- `aria-roles-valid` and `aria-deprecated-role` missed a hidden shadow host. Their ancestor walk used `parentElement`, which stops at a shadow root, so a host carrying `aria-hidden` was never seen from inside its own shadow content. The walk now follows the composed tree, stepping over the shadow root to reach the host.
|
|
80
|
+
- `aria-roles-valid` and `aria-deprecated-role` now treat an `inert` subtree as programmatically hidden, alongside `display:none`, `visibility` and `aria-hidden`. The ACT glossary those two follow predates `inert` and names only the other three, but an inert subtree is out of the accessibility tree entirely, so a role on it reaches nobody. Rules that judge attribute syntax are unchanged, since that's a static-markup property regardless of what's visible today.
|
|
81
|
+
- The committed WCAG coverage report was stale, still describing `bypass-blocks-present` as an automatic rule under `src/checks/automatic/` after it became manual. The generator stamped a generation time into both files, so every run produced a diff and real drift had nowhere to show. The timestamp is gone (git records when the file changed), output is byte-stable across runs, and `npm run coverage:check` fails on drift the way the ARIA and language-subtag generators already do. CI runs it.
|
|
82
|
+
- A `runOnly` filter given as a comma-separated string (`includeRuleIds: 'img-alt-present'`) was silently dropped and every rule ran. The normalizer had always accepted a string; the check deciding whether `runOnly` carried any filters at all only recognised arrays, so the filter was parsed and then discarded. `ENGINE_OPTIONS.md` states these fields mirror their `engineOptions` counterparts, which have always taken a string.
|
|
83
|
+
- A partial `engineOptions.messages` entry replaced the built-in dictionary for that locale instead of layering over it, so overriding one German string silently returned every *other* string in English, the opposite of what `docs/I18N.md` promised. A supplied dictionary now sits on top of the built-in one for the same locale, and completeness counts both layers.
|
|
84
|
+
- `engineOptions.locale` set to an inherited property name (`constructor`, `__proto__`, `toString`) was reported as the resolved locale, because the built-in table was tested for truthiness rather than for an own property holding a dictionary. Text stayed English; only `engine.locale` was wrong.
|
|
85
|
+
- A malformed `engineOptions.messages` entry (`{ de: null }`, a string, an array) could crash a scan rather than being ignored.
|
|
86
|
+
- `npm run validate:automatic-rules` and `validate:manual-rules` both failed, and had for some time, because nothing ran them. Three bugs: the module contract asserted rules export *exactly* `id`, `meta`, `runInPage`, but 14 legitimately export `applicability` too; the free-variable scan stripped single-quoted strings before double-quoted ones, so an apostrophe inside a double-quoted hint opened a phantom string and exposed the code after it; and regex literals weren't stripped at all, so `replace(/"/g, ...)` did the same thing with its quote. All three are fixed and both validators now run in CI as `npm run validate:rules`. `CONTRIBUTING.md` described the wrong contract and has been corrected.
|
|
87
|
+
- `docs/RULE_TEMPLATE.js` told rule authors to name i18n keys `checks.<ruleId>.occurrence.<case>.summary`. No key in `en.json` has ever used that shape: 124 of the 125 rules use `<ruleName>_summary_<outcome>`. The template now states the real convention.
|
|
88
|
+
- `aria-deprecated-role`'s hint was English in every locale. Its two hint strings were `"{{guidance}}"`, a bare placeholder, and the text behind it was a hardcoded English literal passed in as a parameter, so a German, Spanish or French scan returned that language's output with one English sentence in it. The guidance now lives in the dictionaries and is translated in all four locales. `tests/i18n/i18n-translatable-strings.test.js` fails any dictionary value that carries no translatable text.
|
|
89
|
+
- `duplicate-id-aria` now reports `cantTell` instead of `fail`. A duplicated id doesn't break the reference: it resolves to the first element in tree order, so the name is still computed. Whether that's the element the author meant isn't in the markup. WCAG 4.1.1, which covered duplicate ids outright, was removed in 2.2; what's left is 4.1.2, and that needs the name to be demonstrably wrong.
|
|
90
|
+
- `duplicate-id-aria` reported duplicates that sat outside the scanned scope, so a run using `contextSelector` or `excludeSelectors` flagged elements it was never asked about. Detection stays document-wide, since id uniqueness is a document property, but occurrences are now limited to the scanned scope.
|
|
91
|
+
|
|
7
92
|
## [1.4.1] - 2026-08-13
|
|
8
93
|
|
|
9
94
|
### Added
|
|
10
95
|
- `scripts/generate-aria-tables.js` generates `aria-allowed-attr`'s global/per-role/implicit-role tables from the `aria-query` package (a devDependency, output committed) instead of hand-maintaining them, with a `--check` mode that fails when the committed tables go stale.
|
|
11
|
-
- `scripts/generate-language-subtags.js` writes the IANA primary language subtags into `dom-helpers` from the `language-subtag-registry` package
|
|
96
|
+
- `scripts/generate-language-subtags.js` writes the IANA primary language subtags into `dom-helpers` from the `language-subtag-registry` package, same `--check` convention. New shared `helpers.isValidLanguageTag` backs both `valid-lang` and `html-lang-attr-present`.
|
|
12
97
|
|
|
13
98
|
### Changed
|
|
14
|
-
- `form-control-single-label` now grades by whether surplus `<label>`s actually compete for the accessible name, instead of always failing on label count: passes when an `aria-labelledby`/`aria-label` override supersedes the native labels
|
|
15
|
-
- `bypass-blocks-present` is now a manual rule (`cantTell`-capped) instead of automatic (`fail`-capable). WCAG 2.4.1 is about blocks "repeated on multiple Web pages"
|
|
16
|
-
- The concrete and abstract ARIA role sets in `aria-helpers` are now generated from `aria-query` too, alongside the attribute tables, so Digital Publishing roles (`doc-biblioref`, etc.) are recognised instead of reported as unknown.
|
|
99
|
+
- `form-control-single-label` now grades by whether surplus `<label>`s actually compete for the accessible name, instead of always failing on label count: passes when an `aria-labelledby`/`aria-label` override supersedes the native labels; `cantTell` when one non-empty label is joined by empty label associations with no override; fails only when two or more non-empty labels genuinely compete. Uses the same shared `labelContributesAccessibleName` helper as `form-control-programmatic-label-present`, so the two agree. Clears a false positive on the Angular Material selectable-card pattern, where a card adds an empty `<label for>` on top of Material's own label while the control is named by `aria-label`.
|
|
100
|
+
- `bypass-blocks-present` is now a manual rule (`cantTell`-capped) instead of automatic (`fail`-capable). WCAG 2.4.1 is about blocks "repeated on multiple Web pages", and whether a block is actually repeated across the site isn't decidable from one document. A single-snapshot scan can also catch a page mid-modal, when the real `<main>`/headings are correctly `aria-hidden`/`inert` for that state and only the dialog is exposed. Finding a recognized mechanism (a main landmark, a working same-page anchor, or a heading) still resolves to `notApplicable`; finding none now returns `cantTell` instead of asserting a violation the engine can't confirm.
|
|
101
|
+
- The concrete and abstract ARIA role sets in `aria-helpers` are now generated from `aria-query` too, alongside the attribute tables, so Digital Publishing roles (`doc-biblioref`, etc.) are recognised instead of reported as unknown.
|
|
17
102
|
- `aria-roles-valid` and `aria-deprecated-role` now skip programmatically hidden elements, where a role has no effect (ACT 674b10).
|
|
18
103
|
- `autocomplete-valid` now follows ACT 73f2c2's applicability: the `on`/`off` toggle, disabled controls (including `aria-disabled`), and input types with a fixed value (`submit`, `checkbox`, ...) are out of scope, since `autocomplete` can't describe a purpose for any of them.
|
|
19
|
-
- `meta-viewport-zoom-enabled` now applies only when `content` sets `maximum-scale` or `user-scalable
|
|
104
|
+
- `meta-viewport-zoom-enabled` now applies only when `content` sets `maximum-scale` or `user-scalable`, since setting neither can't restrict zoom.
|
|
20
105
|
- `aria-allowed-attr` now covers all 127 concrete ARIA roles instead of 35 hand-listed ones, so `button`, `link`, `img` and 27 other common roles are no longer skipped.
|
|
21
106
|
- Form-control naming is now split by native element vs. explicit ARIA role, so a control isn't reported by two rules at once: `binary-control-name-present`/`slider-name-present` are role-only, and `form-control-programmatic-label-present` skips a control whose explicit role has its own naming rule. An unlabelled checkbox used to produce two findings; now one.
|
|
22
107
|
|
|
23
108
|
### Fixed
|
|
24
|
-
- `contrast-minimum`/`contrast-enhanced` no longer report text hidden with the sr-only clip technique (`clip: rect(0,0,0,0)`/`clip-path: inset(50%+)
|
|
25
|
-
- `aria-allowed-attr` failed the four ARIA 1.2 ex-globals (`aria-disabled`, `aria-errormessage`, `aria-haspopup`, `aria-invalid`) on any role that doesn't support them, but they're deprecated on that role, not prohibited
|
|
26
|
-
- `aria-deprecated-role` failed `role="generic"`, but WAI-ARIA 1.2 §5.4 states that rule at SHOULD-NOT strength ("primarily for implementors of user agents")
|
|
27
|
-
- `nested-interactive-controls-absent` matched on role membership alone, so it failed a `role="listbox"` owning `role="option"` children, a `tablist` owning `tab`s, and every other composite widget whose managed children legitimately carry a widget role
|
|
28
|
-
-
|
|
29
|
-
- `avoid-inline-spacing` failed every inline `line-height`/`letter-spacing`/`word-spacing` declared `!important
|
|
30
|
-
- `table-th-has-data-cells` failed every `<th>` in any table with zero `<td>`, so a `role="presentation"` layout table carrying a stray `<th>`, a
|
|
31
|
-
- `label-in-name` compared label and name by character containment (`indexOf` on lowercased strings), which is wrong in both directions per WCAG 2.5.3's
|
|
32
|
-
- `img-alt-present`, `object-text-alternative-present` and `canvas-text-alternative-present` used `isAccTreeEligible`, which keeps a tabbable element eligible under `aria-hidden`, so they reported elements no assistive technology can reach. All three now use `isIncludedInAccessibilityTree`, matching the `*-name-present` rules
|
|
33
|
-
- `aria-roles-valid` judged only the first token of the `role` attribute, so `role="searchfield searchbox"` was reported invalid
|
|
34
|
-
- `autocomplete-valid` accepted a contact modality token before a non-contact field, so `"work photo"` passed even though a contact token is only valid when a contact field follows it
|
|
35
|
-
- `valid-lang` and `html-lang-attr-present` checked shape only (`/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/`), so `lang="eng"` and `lang="em-US"` passed although neither is a registered primary subtag
|
|
36
|
-
- `meta-viewport-zoom-enabled` passed values it couldn't parse (`user-scalable=0.5`, `maximum-scale=invalid`, `maximum-scale=yes`), even though CSS Device Adaptation treats an unparseable value as `0
|
|
37
|
-
- `meta-refresh-timing-absent` and `meta-refresh-no-exceptions` reported directives a browser never acts on (`"foo; URL=x"`, `"+72001"`, `"0:1"`), since a malformed value
|
|
38
|
-
- `aria-allowed-attr` only looked at `[role]`, so an ARIA state/property on an element with no explicit role went unjudged (e.g. `<button aria-sort>`, `<p aria-checked>`) even though ACT 5c01ea applies to any element in the accessibility tree. The generator now also emits an implicit-role table, gated by an explicit allowlist of elements whose role is unconditional
|
|
39
|
-
- `aria-allowed-attr` treated `aria-disabled`, `aria-haspopup`, `aria-invalid` and `aria-errormessage` as global attributes, so misuse on a role that doesn't support them went unreported; the generated table has the real 17 globals. It also no longer judges attributes against `role="none"`/`"presentation"`, since
|
|
40
|
-
- `input-image-alt-present` treated `alt=""` as marking an image button decorative and passed it, but an image button is a control, not decoration
|
|
41
|
-
- `input-image-alt-present` now scopes to elements included in the accessibility tree (was the looser `isAccTreeEligible`), and rejects an accessible name equal to the browser's own fallback for an image button (`"Submit Query"`/`"Submit"`) as no name at all, per ACT 59796f.
|
|
42
|
-
- `form-control-programmatic-label-present` used the looser `isAccTreeEligible` check,
|
|
43
|
-
- The 18 `*-name-present` rules judged an element whether or not it was actually reachable by assistive technology, so a focusable element inside `aria-hidden`
|
|
44
|
-
- `link-name-present`/`button-name-present` credited an accessible name from content regardless of an explicit `role`, so `<a href role="alert">Text</a>` passed even though
|
|
45
|
-
- `landmark-banner-is-top-level` and `landmark-contentinfo-is-top-level` selected any roleless `<header>`/`<footer>` as a candidate, regardless of nesting
|
|
46
|
-
- `getContentNameInfo` let a descendant's `title` outrank its own text when computing a name from subtree content, so `<a title="T">Text</a>` could name itself `"T"` instead of `"Text"`. Content now wins over `title` unless the descendant's content is empty, matching
|
|
109
|
+
- `contrast-minimum`/`contrast-enhanced` no longer report text hidden with the sr-only clip technique (`clip: rect(0,0,0,0)`/`clip-path: inset(50%+)`), since there's no visually-presented color to check. `opacity: 0` and off-screen positioning stay in scope, since either can be a single-property mistake on text meant to be visible, unlike the multi-property clip pattern.
|
|
110
|
+
- `aria-allowed-attr` failed the four ARIA 1.2 ex-globals (`aria-disabled`, `aria-errormessage`, `aria-haspopup`, `aria-invalid`) on any role that doesn't support them, but they're deprecated on that role, not prohibited, still allowed just discouraged. Now reports `cantTell` (reason `ARIA_ATTR_DEPRECATED`) for those four instead of `fail`; a genuinely unsupported attribute still fails.
|
|
111
|
+
- `aria-deprecated-role` failed `role="generic"`, but WAI-ARIA 1.2 §5.4 states that rule at SHOULD-NOT strength ("primarily for implementors of user agents"), so the usage is conforming. Now reports `cantTell` under reason code `ARIA_ROLE_AUTHOR_DISCOURAGED`. `fail` is retained for a role carrying an author MUST NOT; no ARIA 1.2/1.3 role does, outside the abstract roles `aria-roles-valid` already covers. See `docs/ARIA_DEPRECATION.md`.
|
|
112
|
+
- `nested-interactive-controls-absent` matched on role membership alone, so it failed a `role="listbox"` owning `role="option"` children, a `tablist` owning `tab`s, and every other composite widget whose managed children legitimately carry a widget role, even when driven entirely via `aria-activedescendant` with nothing separately focusable inside. A descendant now counts only when it's also focusable (`helpers.getFocusableInfo`, accounting for `contenteditable`, `:disabled`, `inert`, invalid/negative `tabindex`).
|
|
113
|
+
- The same rule's focusability gate still over-flagged the roving-tabindex pattern, where the active owned child genuinely carries `tabindex="0"` despite being a managed part of its container. Added an explicit owned-child map (`option`→`listbox`/`combobox`, `tab`→`tablist`, `treeitem`→`tree`, `menuitem(checkbox|radio)`→`menu`/`menubar`, `radio`→`radiogroup`); a child matching both the role and a matching ancestor container is exempt regardless of its own tabindex.
|
|
114
|
+
- `avoid-inline-spacing` failed every inline `line-height`/`letter-spacing`/`word-spacing` declared `!important` regardless of value, so `line-height: 2em !important` on 16px text was reported even though it already exceeds what WCAG 1.4.12 asks for. Now fails only below 1.5× font size for line-height, 0.12× for letter-spacing, 0.16× for word-spacing.
|
|
115
|
+
- `table-th-has-data-cells` failed every `<th>` in any table with zero `<td>`, so a `role="presentation"` layout table carrying a stray `<th>`, or a `<th>` that was `display:none`/`aria-hidden`, or a `<th role="cell">`, were all reported. Per ACT d0f69e a header cell now counts only while visible, in the accessibility tree, and not overridden to a role other than `rowheader`/`columnheader`.
|
|
116
|
+
- `label-in-name` compared label and name by character containment (`indexOf` on lowercased strings), which is wrong in both directions per WCAG 2.5.3's word-based algorithm: it accepted `aria-label="Discover Italy"` for the visible text "Discover It", and rejected `aria-label="Search by date (YYYY-MM-DD)"` against "Search by date". Now compares word lists (parenthesised text stripped, case-folded, NFKD-normalised, non-alphanumerics collapsed to spaces) and checks the label's words appear adjacent and in order in the name. An abbreviation or a differently-hyphenated word now reports `cantTell` instead of failing.
|
|
117
|
+
- `img-alt-present`, `object-text-alternative-present` and `canvas-text-alternative-present` used `isAccTreeEligible`, which keeps a tabbable element eligible under `aria-hidden`, so they reported elements no assistive technology can reach. All three now use `isIncludedInAccessibilityTree`, matching the `*-name-present` rules.
|
|
118
|
+
- `aria-roles-valid` judged only the first token of the `role` attribute, so `role="searchfield searchbox"` was reported invalid even though `role` takes a fallback list and a browser resolves it to the first token it recognises. The rule now fails only when no token names a concrete role.
|
|
119
|
+
- `autocomplete-valid` accepted a contact modality token before a non-contact field, so `"work photo"` passed even though a contact token is only valid when a contact field follows it.
|
|
120
|
+
- `valid-lang` and `html-lang-attr-present` checked shape only (`/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/`), so `lang="eng"` and `lang="em-US"` passed although neither is a registered primary subtag. Both now validate against the real IANA registry. `valid-lang` also now applies only where text actually inherits the language from the element, and a whitespace-only value fails.
|
|
121
|
+
- `meta-viewport-zoom-enabled` passed values it couldn't parse (`user-scalable=0.5`, `maximum-scale=invalid`, `maximum-scale=yes`), even though CSS Device Adaptation treats an unparseable value as `0`, which disables zoom exactly like an explicit `0` does.
|
|
122
|
+
- `meta-refresh-timing-absent` and `meta-refresh-no-exceptions` reported directives a browser never acts on (`"foo; URL=x"`, `"+72001"`, `"0:1"`), since a malformed value invalidates the whole directive rather than falling back to a default delay. Both now parse `content` with the shared declarative-refresh steps.
|
|
123
|
+
- `aria-allowed-attr` only looked at `[role]`, so an ARIA state/property on an element with no explicit role went unjudged (e.g. `<button aria-sort>`, `<p aria-checked>`) even though ACT 5c01ea applies to any element in the accessibility tree. The generator now also emits an implicit-role table, gated by an explicit allowlist of elements whose role is unconditional.
|
|
124
|
+
- `aria-allowed-attr` treated `aria-disabled`, `aria-haspopup`, `aria-invalid` and `aria-errormessage` as global attributes, so misuse on a role that doesn't support them went unreported; the generated table has the real 17 globals. It also no longer judges attributes against `role="none"`/`"presentation"`, since `presentation-role-conflict` already owns that case.
|
|
125
|
+
- `input-image-alt-present` treated `alt=""` as marking an image button decorative and passed it, but an image button is a control, not decoration: ACT 59796f requires a non-empty name, so an empty `alt` (or no `alt` at all) now fails. `alt=""` combined with `aria-label`/`aria-labelledby`/`title` still passes.
|
|
126
|
+
- `input-image-alt-present` now scopes to elements included in the accessibility tree (was the looser `isAccTreeEligible`), and rejects an accessible name equal to the browser's own fallback for an image button (`"Submit Query"`/`"Submit"`) as no name at all, per ACT 59796f.
|
|
127
|
+
- `form-control-programmatic-label-present` used the looser `isAccTreeEligible` check, so it reported controls no assistive technology can reach. Switched to `isIncludedInAccessibilityTree`, matching the `*-name-present` rules.
|
|
128
|
+
- The 18 `*-name-present` rules judged an element whether or not it was actually reachable by assistive technology, so a focusable element inside `aria-hidden` got a real pass/fail verdict even though such elements aren't in the accessibility tree at all. New shared `helpers.isIncludedInAccessibilityTree` routes all 18 rules to `notApplicable` there instead; the focus-order defect itself is still reported by `aria-hidden-focus`.
|
|
129
|
+
- `link-name-present`/`button-name-present` credited an accessible name from content regardless of an explicit `role`, so `<a href role="alert">Text</a>` passed even though a real accessibility tree computes an empty name for it. Naming from content is now gated by the ARIA 1.2 §5.2.8.5 allowlist of roles that actually support it, generated from each role's `nameFrom` so a role that inherits the behavior (like `doc-noteref` from `link`) is covered too.
|
|
130
|
+
- `landmark-banner-is-top-level` and `landmark-contentinfo-is-top-level` selected any roleless `<header>`/`<footer>` as a candidate, regardless of nesting, but per HTML-AAM those elements have no banner/contentinfo role at all once descended from `article`/`aside`/`main`/`nav`/`section`. Both now select through the same suppression-aware role lookup the rest of the file already used.
|
|
131
|
+
- `getContentNameInfo` let a descendant's `title` outrank its own text when computing a name from subtree content, so `<a title="T">Text</a>` could name itself `"T"` instead of `"Text"`. Content now wins over `title` unless the descendant's content is empty, matching the accname spec.
|
|
47
132
|
|
|
48
133
|
## [1.4.0] - 2026-08-08
|
|
49
134
|
|
|
50
135
|
### Added
|
|
51
|
-
- `tests/i18n/i18n-locale-completeness.test.js`: an automated check for the key-parity drift `docs/I18N.md` warned about but never enforced
|
|
52
|
-
- `src/i18n/de.js` and `src/i18n/es.js`: two new fully-translated (614/614 keys) locales, German and Spanish, joining `en`/`fr`.
|
|
53
|
-
- `npm run i18n:new <locale>` (`scripts/i18n-scaffold.js`) and `npm run i18n:report` (`scripts/i18n-report.js`): tooling
|
|
54
|
-
- 107 new direct unit tests targeting previously-uncovered branches in
|
|
136
|
+
- `tests/i18n/i18n-locale-completeness.test.js`: an automated check for the key-parity drift `docs/I18N.md` warned about but never enforced. It fails the build for any locale file with an orphaned key not present in `en.js`, and separately fails if a locale listed in `FULLY_TRANSLATED_LOCALES` (currently just `fr`) is missing any `en.js` key.
|
|
137
|
+
- `src/i18n/de.js` and `src/i18n/es.js`: two new fully-translated (614/614 keys) locales, German and Spanish, joining `en`/`fr`. Documented in `docs/I18N.md`'s coverage table and mentioned in `README.md`'s "Localized reporting" principle.
|
|
138
|
+
- `npm run i18n:new <locale>` (`scripts/i18n-scaffold.js`) and `npm run i18n:report` (`scripts/i18n-report.js`): tooling for community translation contributions. `i18n:new` scaffolds `src/i18n/<locale>.js` pre-populated with every `en.js` key, seeded with the English text as a placeholder. `i18n:report` prints per-locale progress. `docs/I18N.md`'s "Contributing a translation" section and `CONTRIBUTING.md` now point at this workflow instead of manual file creation.
|
|
139
|
+
- 107 new direct unit tests targeting previously-uncovered branches in `src/core/dom-helpers.js` and `src/core/contrast-helpers.js`. Coverage: `dom-helpers.js` 86.34%/67.91%/92.11% → 92.68%/74.26%/96.49% (lines/branches/functions); `contrast-helpers.js` 94.70%/71.02%/100% → 100%/84.76%/100%. No production-code changes resulted; every previously-untested branch was confirmed correct against spec, sibling-function behavior, or existing fixtures.
|
|
55
140
|
|
|
56
141
|
### Fixed
|
|
57
|
-
- `en.js`'s `mediaTranscriptPresent_summary_cantTell_missing` key contained French text
|
|
58
|
-
- `buildSimpleSelector` (
|
|
59
|
-
- `computeIdRefTargetTextAlternative` (
|
|
60
|
-
- `embed-text-alternative-quality-manual` treated a merely
|
|
61
|
-
- `heading-order-manual`, `landmark-banner-is-top-level-manual`, `landmark-contentinfo-is-top-level-manual`, and `landmark-main-is-top-level-manual` never filtered
|
|
62
|
-
- `image-redundant-alt-manual` collected a candidate `<img>`'s sibling text unconditionally
|
|
63
|
-
- `label-title-only-manual` checked only whether a `<label for
|
|
64
|
-
- `empty-table-header-manual` computed a header cell's
|
|
65
|
-
- `table-fake-caption-manual` treated an `aria-hidden` `<tr>` as the table's positional
|
|
66
|
-
- `td-has-header`
|
|
67
|
-
- `nested-interactive-controls-absent`'s nested-descendant search used
|
|
68
|
-
- `iframe-focusable-content`'s `hasFocusableCandidate` never checked whether a candidate inside a `tabindex="-1"` frame's embedded document was actually rendered
|
|
69
|
-
- `aria-helpers.js`'s `hasAccessibleNameHint` (decides whether a `<section>` resolves to the `'section[named]'` role key,
|
|
70
|
-
- `contrast-helpers.js`'s `getComputabilityBlocker` treated `backdrop-filter` the same as
|
|
71
|
-
- `aria-helpers.js`'s `validateAttrValue` treated an explicitly-
|
|
72
|
-
- `svg-text-alternative-present`'s applicability gate only recognized `role="img"` as an
|
|
73
|
-
- `aria-prohibited-attr`'s
|
|
74
|
-
- `isAccTreeEligible`
|
|
75
|
-
- `presentation-role-conflict-manual`'s conflicting-attribute check treated `aria-hidden="true"`
|
|
76
|
-
- `listitem-parent-valid`'s applicability check inspected the `<li>`'s parent for an explicit role override but never the `<li>`
|
|
77
|
-
- `aria-helpers.js`'s `ALLOWED_ROLES_BY_ELEMENT.button` list was missing `gridcell`, `separator`, `slider`, and `treeitem
|
|
78
|
-
- `focus-order-semantics-manual`'s `NON_INTERACTIVE_ROLES` set included `region`, flagging a tabbable `role="region"`
|
|
79
|
-
- `aria-helpers.js`'s `CONCRETE_ROLES` registry (backing `isValidConcreteRole`, which gates `aria-roles-valid` and 7 other rules) was scoped to core WAI-ARIA 1.2
|
|
142
|
+
- `en.js`'s `mediaTranscriptPresent_summary_cantTell_missing` key contained French text instead of English, so even a default English scan showed French for this one `cantTell` string. Replaced with the correct English string, matching the rule's own hardcoded fallback text and the placeholder convention used elsewhere in the file. `fr.js` was unaffected; `de.js`/`es.js` were generated correctly from the start.
|
|
143
|
+
- `buildSimpleSelector` (the bare-tag/attribute-anchor fallback `buildSelector` degrades to once every other anchoring strategy fails) had the same raw-vs-trimmed bug as `buildSelectorUncached`'s anchor builders (see the 1.3.0 `buildSelector` fix below), embedding the *trimmed* id/data-testid/name attribute value into the selector while only using the trimmed value to check truthiness, so a padded attribute produced a selector that could never resolve back to its element. Fixed by embedding the raw, untrimmed value in all three branches.
|
|
144
|
+
- `computeIdRefTargetTextAlternative` (resolves what an `aria-labelledby`/`aria-describedby` target itself contributes) had two bugs: it checked the target's own `aria-label` before its own `aria-labelledby`, backwards from the accname spec's ordering, so a target carrying both resolved to the stale one; and it never consulted a native `<label>` association at all, so a target that is itself a labeled form control resolved to empty text. Fixed by reordering the checks and adding the same native-label lookup `getAccessibleNameInfo` uses. A follow-up fix closed a cycle-detection gap this surfaced: a label whose content self-referenced back to the control it labels could round-trip and produce doubled text; an `opts.__idrefVisited` guard now threads through the resolution chain to prevent it.
|
|
145
|
+
- `embed-text-alternative-quality-manual` treated a merely-present (even broken/empty-resolving) `aria-labelledby` as a mechanism worth reviewing, producing a confusing `cantTell` for an `<embed>` that in fact has no text alternative at all. Its sibling rules (`object-`/`svg-`/`canvas-text-alternative-quality-manual`) already required the reference to resolve to non-empty text; `embed` alone disagreed. Now matches.
|
|
146
|
+
- `heading-order-manual`, `landmark-banner-is-top-level-manual`, `landmark-contentinfo-is-top-level-manual`, and `landmark-main-is-top-level-manual` never filtered candidates through `helpers.isAccTreeEligible`, so an `aria-hidden` heading or landmark (visually rendered but removed from the AT-perceived structure) was wrongly flagged, and for `heading-order` could also mask a real level skip immediately after it. All four now filter through `isAccTreeEligible`.
|
|
147
|
+
- `image-redundant-alt-manual` collected a candidate `<img>`'s sibling text unconditionally, including `aria-hidden` sibling text that assistive technology never announces, so it couldn't actually cause the double-announcement this rule exists to catch. Now skips AT-ineligible siblings when building the comparison.
|
|
148
|
+
- `label-title-only-manual` checked only whether a `<label for>`/wrapping `<label>` structurally existed, never whether it contributed a name, so an empty label exempted the control even though `title` was functionally its only real label. Rewritten to delegate to `helpers.getAccessibleNameInfo` and to also filter candidates through `isAccTreeEligible`.
|
|
149
|
+
- `empty-table-header-manual` computed a header cell's visible text via plain `el.textContent`, which includes `aria-hidden` descendant text a screen reader never announces. The text walk now skips AT-ineligible descendants, and a fully `aria-hidden` header cell is excluded as a candidate.
|
|
150
|
+
- `table-fake-caption-manual` treated an `aria-hidden` `<tr>` as the table's positional first row and counted `aria-hidden` cells toward a row's cell count. Rows and cells are now filtered through `isAccTreeEligible` first.
|
|
151
|
+
- `td-has-header` credited an `aria-hidden` `<th>` as a valid implicit header for other cells, so a `<td>` relying solely on one was wrongly reported `pass` when it has no accessible header at all (a false negative on a `serious` WCAG 1.3.1 check). An `aria-hidden` `<th>` no longer counts as a header; an `aria-hidden` `<td>` is no longer flagged either.
|
|
152
|
+
- `nested-interactive-controls-absent`'s nested-descendant search used raw `querySelectorAll` with no hidden-content filtering at all, so a `display:none` or non-focusable-`aria-hidden` nested control was wrongly failed. Both the outer candidate and the nested search now filter through `isAccTreeEligible`; a control that's `aria-hidden` but still tabbable correctly remains flagged, since that's the separate anti-pattern `aria-hidden-focus` targets.
|
|
153
|
+
- `iframe-focusable-content`'s `hasFocusableCandidate` never checked whether a candidate inside a `tabindex="-1"` frame's embedded document was actually rendered, so a `display:none`/`visibility:hidden`/`[hidden]` element was wrongly reported reachable. Added a self-contained rendering check scoped to genuine non-rendering, not `aria-hidden` (which alone doesn't remove native tab-order reachability).
|
|
154
|
+
- `aria-helpers.js`'s `hasAccessibleNameHint` (decides whether a `<section>` resolves to the `'section[named]'` role key, the only one that permits `role="region"`) only checked `aria-label`/`aria-labelledby`, not `title`, inconsistent with this engine's own `getLandmarkNameInfo` precedence. A `<section title="...">` was wrongly failed by `aria-allowed-role` for an explicit `role="region"` restatement. Now matches `getLandmarkNameInfo`'s precedence.
|
|
155
|
+
- `contrast-helpers.js`'s `getComputabilityBlocker` treated `backdrop-filter` the same as `filter`/`mix-blend-mode`/ancestor `opacity`, none occludable by a closer opaque ancestor background. That reasoning doesn't apply to `backdrop-filter`, which samples whatever's already rendered behind the element: a closer opaque `background-color` paints over that filtered result and hides it completely, the same physical occlusion `background-image` gets. `backdrop-filter` now participates in the same `paintOccluded` short-circuit as `background-image`/gradient; plain `filter`/`mix-blend-mode`/`opacity` remain unconditional blockers.
|
|
156
|
+
- `aria-helpers.js`'s `validateAttrValue` treated an explicitly-empty idref/idref-list ARIA attribute value (e.g. `aria-describedby=""`) as invalid. An empty value is a deliberate "no reference", not a broken one, and both the `idref` and `idref-list` cases now treat it as valid.
|
|
157
|
+
- `svg-text-alternative-present`'s applicability gate only recognized `role="img"` as an intent-to-convey signal for an `<svg>` root, missing `role="graphics-symbol"` and `role="graphics-document"`. Both roles are now recognized, still scoped to the `<svg>` root element only.
|
|
158
|
+
- `aria-prohibited-attr`'s roleless-element branch only recognized a small allowlist of native HTML tags as having no implicit role, never autonomous custom elements, which per the Custom Elements spec always have no implicit ARIA role. Fixed by adding a second applicability path: any tag containing a hyphen, except the small set of legacy hyphenated SVG/MathML tag names that predate Custom Elements (`annotation-xml`, `color-profile`, `font-face` and its variants, `missing-glyph`).
|
|
159
|
+
- `isAccTreeEligible` treated `hidden="until-found"` identically to a plain `hidden` attribute, excluding the element itself from every rule's candidate list. Per the HTML spec these differ: the UA stylesheet applies `content-visibility: hidden` for `until-found` (hides descendants, not the element carrying it) vs. `display: none` for any other `hidden` value. Fixed with a self-only override: the element carrying `hidden="until-found"` is no longer excluded when it's the one being checked; a real descendant, or any element with a plain `hidden` attribute, is unaffected.
|
|
160
|
+
- `presentation-role-conflict-manual`'s conflicting-attribute check treated `aria-hidden="true"` the same as any other global ARIA attribute, flagging it as restoring the implicit role, but `aria-hidden="true"` unconditionally removes the element (and any other attribute alongside it) from the accessibility tree regardless of role, so no assistive technology ever sees what this check warned about. An element's own `aria-hidden="true"` now clears its conflicting-attribute list entirely; focusability is unaffected and still flags on its own. `aria-hidden=""` (empty/invalid, doesn't hide) is unaffected and still triggers normally.
|
|
161
|
+
- `listitem-parent-valid`'s applicability check inspected the `<li>`'s parent for an explicit role override but never the `<li>` itself, wrongly flagging `<li role="tab">`/`role="menuitem">`/`role="presentation">` etc. inside an invalid parent, even though an explicit role fully overrides the `<li>`'s native listitem role. An `<li>` whose own explicit role isn't empty or `"listitem"` is no longer evaluated at all.
|
|
162
|
+
- `aria-helpers.js`'s `ALLOWED_ROLES_BY_ELEMENT.button` list was missing `gridcell`, `separator`, `slider`, and `treeitem`, confirmed against the W3C "ARIA in HTML" normative table. Fixed by adding the four missing roles.
|
|
163
|
+
- `focus-order-semantics-manual`'s `NON_INTERACTIVE_ROLES` set included `region`, flagging a tabbable `role="region"` as a meaningless tab stop, though that's a real, common, WCAG 2.1.1/2.1.3-grounded pattern. This engine's own sibling check, `scrollable-region-focusable`, already documents that basis for exactly this pattern, so flagging it here was internally inconsistent. Fixed by removing only `region` from the set; `navigation`/`status`/`tabpanel` remain flagged pending confirmed over-flagging evidence.
|
|
164
|
+
- `aria-helpers.js`'s `CONCRETE_ROLES` registry (backing `isValidConcreteRole`, which gates `aria-roles-valid` and 7 other rules) was scoped to core WAI-ARIA 1.2 tokens only, missing the three WAI-ARIA Graphics Module 1.0 roles (`graphics-document`/`graphics-object`/`graphics-symbol`), a separate W3C Recommendation with its own Accessibility API Mappings REC defining real AT support. `aria-roles-valid` wrongly reported these as invalid. All 8 rules gating on `isValidConcreteRole` key their per-role tables by explicit role name and default to skipping a role absent from the table, so recognizing these 3 as concrete introduces no new false positive in any of them. Digital Publishing WAI-ARIA roles were deliberately left for a future round pending confirmed real-world usage.
|
|
80
165
|
|
|
81
166
|
### Changed
|
|
82
|
-
- **The CLI now ships as a separate package, [`@surea11y/cli`](https://github.com/SureA11y/cli), and `@surea11y/core` has zero runtime dependencies.** `jsdom` was previously a real `dependencies` entry
|
|
83
|
-
- `package.json` now declares an explicit `exports` map: `.`, `./baseline`, `./report`, `./sarif`, `./browser`, `./package.json`. Previously there was no map at all, so
|
|
84
|
-
- `package.json` gained a `keywords` field (18 entries)
|
|
85
|
-
- `files` allowlist tightened from directory-level (`src`) to an explicit per-entry list, dropping ~721 KB of build
|
|
86
|
-
|
|
87
|
-
- A small `bin/surea11y-core.js` stub replaces the removed CLI entry point, so the pre-1.4.0 `npx @surea11y/core scan ...` still shown in older documentation prints an actionable redirect (`npx @surea11y/cli scan <file-or-url>`, exit 2) instead of npm's opaque `could not determine executable to run`. Deliberately **not** named `surea11y`: that binary belongs to `@surea11y/cli`, and since core is a transitive dependency of all seven bindings, the two would land in the same `node_modules/.bin` and collide — npm resolves such a conflict silently, with no warning, so a same-named stub could shadow a working CLI install. `npx` runs a package's single binary regardless of its name, which is what makes the redirect work without claiming the name. It writes only to stderr and adds no dependency.
|
|
167
|
+
- **The CLI now ships as a separate package, [`@surea11y/cli`](https://github.com/SureA11y/cli), and `@surea11y/core` has zero runtime dependencies.** `jsdom` was previously a real `dependencies` entry, pulling 39 transitive packages / ~25 MB into every install, including all six first-party consumers, none of which ever load it since they drive real browsers. The engine reads a DOM it's handed and never constructs one; only the CLI needed to parse HTML into a DOM. Splitting it puts that cost solely on people who install the CLI. **Breaking for CLI users**: `npx @surea11y/core scan ...` no longer exists; use `npx @surea11y/cli scan ...`. **Breaking for the documented Node+jsdom library workflow**: jsdom used to be available implicitly via npm hoisting; it now needs an explicit `npm install jsdom`. No rule logic, rule ID, outcome, or result shape changed. Shipped as a minor rather than a major since the package had no external consumers at this version and all six first-party ones were verified unaffected.
|
|
168
|
+
- `package.json` now declares an explicit `exports` map: `.`, `./baseline`, `./report`, `./sarif`, `./browser`, `./package.json`. Previously there was no map at all, so every internal file was reachable by deep `require()` and therefore implicitly public. The two documented deep imports move from `@surea11y/core/src/baseline`/`src/report` to `@surea11y/core/baseline`/`report`; `src/core/*`, `src/checks/*`, `src/i18n/*`, `src/policy/*` and the generated `src/core.js` are now sealed. Documented in `docs/API_STABILITY.md`'s "Package entry points" section.
|
|
169
|
+
- `package.json` gained a `keywords` field (18 entries); the package previously had none and was effectively unfindable via npm search. `description` rewritten from a generic line to lead with the `cantTell` differentiator and the zero-dependency property.
|
|
170
|
+
- `files` allowlist tightened from directory-level (`src`) to an explicit per-entry list, dropping ~721 KB of build inputs that `scripts/build-core.js` already inlines into the generated `src/core.js` and that nothing requires at runtime. `src/checks/**` is kept, since the generated bundle `require()`s all 125 rule files at runtime. Published package: 175 → 152 files, 7.04 → 6.31 MB unpacked, 1.40 → 1.23 MB packed.
|
|
171
|
+
- A small `bin/surea11y-core.js` stub replaces the removed CLI entry point, so the pre-1.4.0 `npx @surea11y/core scan ...` shown in older documentation prints an actionable redirect instead of npm's opaque error. Not named `surea11y`, since that binary belongs to `@surea11y/cli` and the two would otherwise collide in `node_modules/.bin`.
|
|
88
172
|
|
|
89
173
|
### Removed
|
|
90
|
-
- `bin/core.js` and `docs/CLI.md
|
|
174
|
+
- `bin/core.js` and `docs/CLI.md`, moved to the [`@surea11y/cli`](https://github.com/SureA11y/cli) package. The binary name, flags, exit codes, and output formats are unchanged; only the package you install it from did.
|
|
91
175
|
|
|
92
176
|
## [1.3.0] - 2026-08-02
|
|
93
177
|
|
|
94
178
|
### Added
|
|
95
|
-
- `surea11y.browser.js`: a standalone browser bundle, regenerated by `npm run build` (`scripts/build-browser.js`) alongside `src/core.js`. Loading it via a plain `<script>` tag
|
|
96
|
-
- `surea11y scan --sarif <path>`: writes a [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) log for GitHub Code Scanning or another SARIF-consuming dashboard, alongside the existing `--json`/`--html` outputs. `fail` occurrences map to SARIF `error`, `cantTell` to `warning
|
|
97
|
-
- `docs/CI_INTEGRATIONS.md`: ready-to-paste GitHub Actions workflow (basic exit-code gating, a `--baseline`-gated variant, and a SARIF-upload-to-Code-Scanning variant) and a Bitbucket Pipelines step template
|
|
98
|
-
- `surea11y scan --custom-rules <path>`: the CLI now exposes `engineOptions.customRules` (previously library-only
|
|
179
|
+
- `surea11y.browser.js`: a standalone browser bundle, regenerated by `npm run build` (`scripts/build-browser.js`) alongside `src/core.js`. Loading it via a plain `<script>` tag defines one global, `a11ycore`, exposing `runa11yCoreInPage`. Excludes `runa11yCoreAcrossFrames`/`a11yCoreEnableFrameResponder` (cross-frame scanning needs the embedded frame to also load the engine and opt in, and would roughly double the bundle's size for a feature most script-tag consumers won't use; still available via `require('@surea11y/core')`). See `docs/INTEGRATION.md`'s "Pattern 3" and `README.md`'s "Standalone browser bundle" section.
|
|
180
|
+
- `surea11y scan --sarif <path>`: writes a [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) log for GitHub Code Scanning or another SARIF-consuming dashboard, alongside the existing `--json`/`--html` outputs. `fail` occurrences map to SARIF `error`, `cantTell` to `warning`. New module `src/sarif.js`. See `docs/SARIF.md` for the full field mapping.
|
|
181
|
+
- `docs/CI_INTEGRATIONS.md`: ready-to-paste GitHub Actions workflow (basic exit-code gating, a `--baseline`-gated variant, and a SARIF-upload-to-Code-Scanning variant) and a Bitbucket Pipelines step template.
|
|
182
|
+
- `surea11y scan --custom-rules <path>`: the CLI now exposes `engineOptions.customRules` (previously library-only), letting an org register its own rule(s) for a scan without forking the engine. `<path>` is a local JS file, `require()`d directly, exporting a rule descriptor or an array of them. The flag is repeatable. Validated at load time; a missing file, a `require()`-time throw, or a malformed export exits `2` with a clear error.
|
|
99
183
|
|
|
100
184
|
### Changed
|
|
101
|
-
- `src/core/aria-helpers.js`
|
|
102
|
-
- `aria-prohibited-attr` now also flags `aria-label`/`aria-labelledby` on
|
|
185
|
+
- `src/core/aria-helpers.js` is always inlined into the generated `src/core.js` bundle, so Node's coverage tool could never attribute execution back to the module, and it had zero direct unit tests (function coverage 31%). Added `tests/core/aria-helpers.test.js`, covering role classification, `validateAttrValue`'s per-value-type branches, `getElementRoleKey`, and `getContainmentRole`. Function coverage: 31% → 100%.
|
|
186
|
+
- `aria-prohibited-attr` now also flags `aria-label`/`aria-labelledby` on roleless elements (no explicit role, no implicit/native role either), not just the small set of explicitly-role-restated naming-prohibited roles it already covered. A roleless element has naming attributes prohibited unless its tag is on a small allow-list or its closest real ancestor role is a widget-type role. The new branch reports two confidence tiers: a roleless element whose subtree already produces a non-empty accessible name from content is `cantTell` (the naming attribute might be a redundant/intentional override), while one with no other accessible-name source at all is a confident `fail`. A roleless helper element nested inside a real widget-type role is exempted. `getNativeRoleForElement` is now re-exported to back this check.
|
|
103
187
|
- License updated to Mozilla Public License 2.0 (MPL-2.0). `LICENSE`, `package.json`'s `license` field, and `README.md`'s License section updated accordingly.
|
|
104
188
|
|
|
105
189
|
### Fixed
|
|
106
|
-
- `buildSelector`
|
|
107
|
-
- `aria-required-parent`'s `hasAcceptableAncestorContext` treated an immediate `role="group"` ancestor as transparent for `listitem`/`treeitem`
|
|
108
|
-
- `form-control-programmatic-label-present` (via the shared `labelContributesAccessibleName
|
|
109
|
-
- `embed`/`object`/`video-poster-text-alternative-present`'s failing-occurrence `hint` text omitted `title` as a remediation option, even though each rule's own
|
|
110
|
-
- `listbox`/`searchbox`/`spinbutton`/`textbox`/`combobox`/`meter`/`progressbar-name-present`'s failing-occurrence `hint` text told authors to
|
|
111
|
-
- `contrast-minimum` attached a failing occurrence's element metadata
|
|
112
|
-
- `getContentNameInfo`
|
|
113
|
-
- `region` was scoped to
|
|
114
|
-
- `getComputabilityBlocker`
|
|
115
|
-
- `form-control-single-label` counted every `<label>`
|
|
116
|
-
- `aria-hidden-focus` now performs a conservative runtime focus-handoff probe for single-offender `aria-hidden` roots before deciding outcome confidence
|
|
117
|
-
- `getContainmentRole`
|
|
118
|
-
- `contrast-minimum`/`contrast-enhanced`
|
|
119
|
-
- `tests/contrast-helpers.test.js` never actually imported `src/core/contrast-helpers.js
|
|
120
|
-
- `aria-prohibited-attr` and `aria-hidden-focus` both collect two independent confidence tiers in one run
|
|
190
|
+
- `buildSelector` built its id/data-testid/name/aria-label anchor selectors by embedding the *trimmed* attribute value into the CSS selector string, while the uniqueness-index lookup that decided whether to use that anchor also keyed on the trimmed value. A CSS attribute/id selector requires an exact match against the real, untrimmed attribute, so any anchor attribute with leading/trailing whitespace produced a selector that could never match its own element and silently degraded to a bare-tag-name fallback resolving to the wrong element. Fixed by embedding the raw, untrimmed value in the actual selector string across all six anchor sites.
|
|
191
|
+
- `aria-required-parent`'s `hasAcceptableAncestorContext` treated an immediate `role="group"` ancestor as transparent for `listitem`/`treeitem` but never added the tested element's own role to the acceptable-context set at that point, so a standard arbitrarily-deep ARIA tree stopped at the second `treeitem` ancestor and failed. It now mirrors the correct behavior: passing a transparent `group` ancestor also adds the element's own role to the acceptable set from that point on.
|
|
192
|
+
- `form-control-programmatic-label-present` (via the shared `labelContributesAccessibleName`) never checked a `<label>`'s own `title` attribute as a last-resort name source, only its ARIA name and content name, so a structurally-associated label with empty content but a non-empty `title` was treated as not contributing a name. Now also checks `getNonEmptyTitle(lab)` after the aria-name and content-name checks come back empty.
|
|
193
|
+
- `embed`/`object`/`video-poster-text-alternative-present`'s failing-occurrence `hint` text omitted `title` as a remediation option, even though each rule's own `@expectation` lists a title attribute as a valid best-effort fallback and each rule's own `runInPage` accepts it. Fixed in the rule source, `src/i18n/en.js`, and `src/i18n/fr.js`.
|
|
194
|
+
- `listbox`/`searchbox`/`spinbutton`/`textbox`/`combobox`/`meter`/`progressbar-name-present`'s failing-occurrence `hint` text told authors to add visible text as a valid fix, but all seven roles are name-from-author-only per WAI-ARIA, so a developer following the hint would rerun the scan and see the same failure. Fixed in the rule source and both locale files.
|
|
195
|
+
- `contrast-minimum` attached a failing occurrence's element metadata as a non-standard top-level `occurrence.node` field, the only place in the whole rule catalog that did this; its twin `contrast-enhanced` already nests this under `occurrence.data.details`, the documented convention. Now matches.
|
|
196
|
+
- `getContentNameInfo` resolved an image-like descendant's contribution via `getAccessibleNameInfo`, which unconditionally falls back to a `title` attribute, so `title` silently outranked `alt` regardless of whether `alt` was present, including the common case of a correct `alt` alongside an unrelated `title` tooltip silently overriding it. Fixed by checking `getAriaNameInfo` first, then a native `<label>` association for `input[type=image]`, then `alt`, with `alt`'s own present/absent distinction deciding whether `title` is a legitimate last-resort fallback.
|
|
197
|
+
- `region` was scoped to direct children of `<body>` only, which turned out to be nearly inert on the most common real-world page shape, a single root mount `<div>` wrapping everything. Replaced with a recursive walk: descend from `<body>`, stop at landmarks/live regions/dialogs/buttons/`<svg>`/`<iframe>`/resolvable skip-links, collect the first node with genuine own content, then collapse contiguous unplaced content back up to its tightest shared ancestor.
|
|
198
|
+
- `getComputabilityBlocker` walked an element's entire ancestor chain looking for a background-image/gradient and reported it as a computability blocker regardless of whether a closer ancestor's own background-color was already fully opaque and would occlude it. Now tracks whether a closer, blend-mode/filter-free ancestor's background resolved fully opaque and, if so, suppresses `BACKGROUND_IMAGE_OR_GRADIENT` for anything farther out. Deliberately not extended to `mix-blend-mode`/`filter`/`backdrop-filter`/ancestor `opacity`, which are compositing-group operations that a closer opaque layer can't occlude through.
|
|
199
|
+
- `form-control-single-label` counted every associated `<label>` regardless of whether it was actually accessibility-tree-eligible, so a hidden decoy/overlay label still triggered a false "multiple labels" flag. Now filters candidate labels through `helpers.isAccTreeEligible` before counting.
|
|
200
|
+
- `aria-hidden-focus` now performs a conservative runtime focus-handoff probe for single-offender `aria-hidden` roots before deciding outcome confidence, observing a short deterministic scheduling window and tracing `focusin` transitions. If focus is handed off outside the same subtree during that window, the finding is downgraded from `fail` to `cantTell`.
|
|
201
|
+
- `getContainmentRole` treated any `role=""` attribute value as a real ancestor/descendant context role for required-context matching, even when the value isn't a valid recognized ARIA role, though real browser/AT behavior is to ignore an unrecognized value and fall back past it. Now validates the explicit role via `isValidConcreteRole` before accepting it.
|
|
202
|
+
- `contrast-minimum`/`contrast-enhanced` never evaluated `<input type="submit"|"button"|"reset">`'s visible label at all: that label renders from the `value` attribute, not a DOM text node, and these are void elements, so the existing text-walk-based candidate collection was structurally blind to them. Added a second candidate pass over these input types reusing the same eligibility gates as the text-node path.
|
|
203
|
+
- `tests/contrast-helpers.test.js` never actually imported `src/core/contrast-helpers.js`; it hand-copied `parseCssColorToRgba`/`compositeRgba`/`contrastRatio` as a duplicate implementation and tested that instead. It now requires the real module.
|
|
204
|
+
- `aria-prohibited-attr` and `aria-hidden-focus` both collect two independent confidence tiers in one run and both silently discarded every `cantTell`-tier finding whenever at least one `fail`-tier finding also existed on the same page. `target-size-minimum` had a related, worse variant: its uncertain cases were tracked only as a page-level boolean with no occurrence object at all. Added a shared `helpers.resolveTieredOutcome(failOccurrences, cantTellOccurrences, severity)` that all three now use: when any fail-tier finding exists the outcome stays `fail`, but both buckets' occurrences are returned together, each carrying its own distinguishing `reasonCode`.
|
|
121
205
|
|
|
122
206
|
## [1.2.0] - 2026-07-31
|
|
123
207
|
|
|
124
208
|
### Added
|
|
125
|
-
- CLI baseline/allowlist mechanism: `surea11y scan --write-baseline <path>` records every current `fail` occurrence (never fails the build); `surea11y scan --baseline <path>` then gates only on occurrences not already recorded there. Matching identity is `ruleId` + `reasonCode` + the occurrence's `html` snippet (
|
|
126
|
-
- `engineOptions.fragment`: 14 rules that check for
|
|
127
|
-
- Versioned public API contract: `docs/API_STABILITY.md`
|
|
128
|
-
- `surea11y scan --html <path>`: a self-contained, browsable HTML report (`src/report.js`'s `renderHtmlReport`)
|
|
209
|
+
- CLI baseline/allowlist mechanism: `surea11y scan --write-baseline <path>` records every current `fail` occurrence (never fails the build); `surea11y scan --baseline <path>` then gates only on occurrences not already recorded there. Matching identity is `ruleId` + `reasonCode` + the occurrence's `html` snippet (not `selector`/`structuralPath`, both position-derived and liable to shift when unrelated markup changes), multiset-matched so repeated identical violations are counted correctly. `buildBaselineEntries`/`matchBaseline` (`src/baseline.js`) are also usable directly by library consumers. See `docs/BASELINE.md`.
|
|
210
|
+
- `engineOptions.fragment`: 14 rules that check for a page-wide property now correctly report `notApplicable`, instead of a false `fail`/`cantTell`, when a scan is scoped to a subtree narrower than the whole document (via `contextSelector`) or run with `engineOptions.fragment: true`. New `helpers.isWholeDocumentScope()` backs this via each rule's `applicability(ctx)` export. See `docs/ENGINE_OPTIONS.md` and `docs/RULE_AUTHORING.md` §11.2.
|
|
211
|
+
- Versioned public API contract: `docs/API_STABILITY.md` codifies which result-shape fields are covered by semver and what triggers a patch/minor/major bump. Also adds a rule-ID deprecation mechanism: `meta.deprecated`/`meta.deprecation` (`{ replacedBy, reason, sinceVersion }`), validated by `normalizeRuleMeta` and surfaced through `getChecksCatalog()`. A deprecated rule keeps running normally; this is a catalog-level migration signal, not an automatic exclusion.
|
|
212
|
+
- `surea11y scan --html <path>`: a self-contained, browsable HTML report (`src/report.js`'s `renderHtmlReport`), hero summary, findings grouped by rule, a WCAG rollup, and a collapsed technical-data section with a searchable occurrence table. No external requests, dark-mode aware. See `docs/REPORT.md`.
|
|
129
213
|
|
|
130
214
|
### Fixed
|
|
131
|
-
- `aria-prohibited-children` resolved an owned child's role via `getExplicitRole` (explicit `role=""`
|
|
132
|
-
- `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-unique`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, and `region` all computed whether a `<header>`/`<footer>`/`<aside>` sits inside a sectioning-content ancestor
|
|
133
|
-
- `hasLandmarkScopingAncestor`
|
|
134
|
-
- `tabindex`, `heading-order`, `empty-heading`, `empty-table-header`, and `scope-attr-valid` all queried `document.querySelectorAll` directly instead of the shared `helpers.queryAllSmart`, so none of them respected `contextSelector` scoping, `excludeSelectors`, or shadow-DOM traversal
|
|
215
|
+
- `aria-prohibited-children` resolved an owned child's role via `getExplicitRole` (explicit `role=""` only), unlike its sibling `aria-required-children`, which resolves via `getContainmentRole` (falling back to a native-tag map: `li`→listitem, `tr`→row, `td`→cell, etc.). A bare `<li>` with no `role` attribute, the common `<ul role="list"><li>` CSS-reset pattern, was read as roleless and structurally transparent, so the ownership walk could recurse past the listitem boundary and report a focusable descendant several levels down as a disallowed owned child. Now uses `getContainmentRole`, so both rules resolve an owned child's role identically. The fix is general: it applies to every container role whose native-tag counterpart the containment map covers.
|
|
216
|
+
- `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-unique`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, and `region` all computed whether a `<header>`/`<footer>`/`<aside>` sits inside a sectioning-content ancestor by checking the ancestor's tag name alone. The correct algorithm is role-aware: an ancestor's bare tag only counts when it carries no `role` attribute at all; once a role is present, only that role's value decides membership. All 7 rules previously carried a duplicated copy of this tag-only check; they now share one `helpers.hasLandmarkScopingAncestor` implementation.
|
|
217
|
+
- `hasLandmarkScopingAncestor` and the separate local `hasLandmarkAncestor` in the three `*-is-top-level` rules both climbed via `parentElement` with no scope boundary, so a `contextSelector`-scoped scan could be affected by real DOM ancestry outside the analyzed subtree. Both now stop climbing once they reach one of the scan's own resolved roots.
|
|
218
|
+
- `tabindex`, `heading-order`, `empty-heading`, `empty-table-header`, and `scope-attr-valid` all queried `document.querySelectorAll` directly instead of the shared `helpers.queryAllSmart`, so none of them respected `contextSelector` scoping, `excludeSelectors`, or shadow-DOM traversal. All five now delegate to `helpers.queryAllSmart`/`.queryAll`.
|
|
135
219
|
|
|
136
220
|
## [1.1.2] - 2026-07-30
|
|
137
221
|
|
|
138
222
|
### Fixed
|
|
139
|
-
- `accesskeys` no longer over-reports duplicate `accesskey` values when one copy is structurally/CSS hidden by default
|
|
223
|
+
- `accesskeys` no longer over-reports duplicate `accesskey` values when one copy is structurally/CSS hidden by default. Candidate collection now follows the shared helper visibility policy, so only currently eligible elements are grouped unless `engineOptions.includeHiddenElements: true` is set.
|
|
140
224
|
- `skip-link` no longer treats fragment-target existence alone as sufficient. It now also flags skip links whose target exists but is currently unusable (hidden from the accessibility tree), while keeping geometry-based target checks gated to environments that expose reliable layout metrics.
|
|
141
|
-
- `page-has-heading-one` and `bypass-blocks-present` credited a fully non-rendered `<h1>`/`<main>`/heading
|
|
142
|
-
- `button-name-present` and `link-name-present` credited a `<button>`/`<a href>` element's rendered content as its accessible name even when an explicit `role` overrode it to a role
|
|
143
|
-
- `createDomHelpers()`'s element-keyed caches
|
|
144
|
-
- `buildSelector` could emit ambiguous selectors in multi-region scans when the target element was the last same-tag sibling,
|
|
145
|
-
- `queryAllSmart` could retain elements that are structurally hard-hidden when `isAccTreeEligible` short-circuited on an `inert` ancestor before reaching an outer `display:none`/`visibility:hidden`/`content-visibility:hidden` ancestor. It now performs a style-only DOM visibility fallback in that path
|
|
225
|
+
- `page-has-heading-one` and `bypass-blocks-present` credited a fully non-rendered `<h1>`/`<main>`/heading as satisfying the check, since both queried the raw DOM with no visibility filtering. Both now filter candidates through `isAccTreeEligible`, matching `landmark-one-main`'s established precedent.
|
|
226
|
+
- `button-name-present` and `link-name-present` credited a `<button>`/`<a href>` element's rendered content as its accessible name even when an explicit `role` overrode it to a value-bearing role (`combobox`, `listbox`, `textbox`, `slider`, `spinbutton`, `progressbar`, `scrollbar`), which is name-from-author-only per the accname spec. Both checks gated on the native host tag alone, never on whether `role` had overridden it. Both rules now exclude these value-roles from name-from-content; a programmatic name still works normally.
|
|
227
|
+
- `createDomHelpers()`'s element-keyed caches were persisted on `window.__a11ycoreSharedCache` and only initialized once per window/document, not once per run, so a window reused across separate calls (e.g. Jest's jsdom environment) could read back a stale cached value for an element that persists by reference across runs. Rule pass/fail outcomes were always computed against the live DOM; only cached diagnostic data like `occurrences[].html` could go stale. `runCore()` now resets the shared cache at the start of every run.
|
|
228
|
+
- `buildSelector` could emit ambiguous selectors in multi-region scans when the target element was the last same-tag sibling, since the `:nth-of-type()` disambiguator could be omitted on that segment. Selector construction now consistently disambiguates those cases.
|
|
229
|
+
- `queryAllSmart` could retain elements that are structurally hard-hidden when `isAccTreeEligible` short-circuited on an `inert` ancestor before reaching an outer `display:none`/`visibility:hidden`/`content-visibility:hidden` ancestor. It now performs a style-only DOM visibility fallback in that path.
|
|
146
230
|
|
|
147
231
|
### Changed
|
|
148
|
-
- Test coverage tightened for this release cycle: re-enabled and stabilized the previously skipped `role-img-text-alternative-present` i18n assertions
|
|
149
|
-
- Removed a dead `root`/`safeRoot` second argument passed to `helpers.queryAllSmart`/`helpers.queryAll` across 67 rule files: both helpers only ever accepted a single selector argument and scope internally via the run's resolved `contextSelector` roots, so the extra argument was silently ignored
|
|
232
|
+
- Test coverage tightened for this release cycle: re-enabled and stabilized the previously skipped `role-img-text-alternative-present` i18n assertions, and added regression coverage for the inert + outer hard-hidden filtering path in `queryAllSmart`.
|
|
233
|
+
- Removed a dead `root`/`safeRoot` second argument passed to `helpers.queryAllSmart`/`helpers.queryAll` across 67 rule files: both helpers only ever accepted a single selector argument and scope internally via the run's resolved `contextSelector` roots, so the extra argument was silently ignored. No behavior change. Also fixed `docs/RULE_TEMPLATE.md`, which declared `safeRoot` but never referenced it.
|
|
150
234
|
|
|
151
235
|
## [1.1.1] - 2026-07-29
|
|
152
236
|
|
|
153
237
|
### Changed
|
|
154
|
-
- **`engineOptions.includeHiddenElements` (default `false`)**: helper-driven rules now skip elements hidden by `display:none` (on the element or any ancestor), `visibility:hidden`/`collapse`, the `[hidden]` attribute, closed `<details>`, and other structurally-non-rendered content by default
|
|
238
|
+
- **`engineOptions.includeHiddenElements` (default `false`)**: helper-driven rules now skip elements hidden by `display:none` (on the element or any ancestor), `visibility:hidden`/`collapse`, the `[hidden]` attribute, closed `<details>`, and other structurally-non-rendered content by default. Filtering happens upstream in the shared `queryAllSmart` helper, before a rule's own pass/fail logic runs. Set `engineOptions.includeHiddenElements: true` to restore the previous behavior. 10 rule files whose own logic doesn't call the underlying eligibility check directly still inherit this filtering through `queryAllSmart`; their doc comments were updated to say so. See `docs/ENGINE_OPTIONS.md` and `docs/LIMITATIONS.md`.
|
|
155
239
|
|
|
156
240
|
### Fixed
|
|
157
|
-
- `docs/LIMITATIONS.md`: the `<dialog>`/UA-stylesheet-hidden-content note was stale
|
|
241
|
+
- `docs/LIMITATIONS.md`: the `<dialog>`/UA-stylesheet-hidden-content note was stale, claiming static-markup-validity checks still evaluate hidden content, which this release's default change makes no longer true. Corrected to describe the current default and how to opt back in.
|
|
158
242
|
|
|
159
243
|
## [1.1.0] - 2026-07-28
|
|
160
244
|
|
|
161
245
|
### Added
|
|
162
|
-
- `engineOptions.rules[ruleId].excludeSelectors`: rule-scoped exclusions, narrowing candidates for exactly one rule on top of (never instead of) the existing global `excludeSelectors`. Resolves the class of false positive where one rule misfires on a component
|
|
163
|
-
- Completed French (`fr`) localization: translated the 313 remaining `src/i18n/fr.js` keys, bringing French to full parity with English (600/600 keys, up from 287/600).
|
|
246
|
+
- `engineOptions.rules[ruleId].excludeSelectors`: rule-scoped exclusions, narrowing candidates for exactly one rule on top of (never instead of) the existing global `excludeSelectors`. Resolves the class of false positive where one rule misfires on a component while every other rule still needs to see it. See `docs/ENGINE_OPTIONS.md`'s "Rule-scoped `excludeSelectors`" section.
|
|
247
|
+
- Completed French (`fr`) localization: translated the 313 remaining `src/i18n/fr.js` keys, bringing French to full parity with English (600/600 keys, up from 287/600).
|
|
164
248
|
|
|
165
249
|
### Fixed
|
|
166
|
-
- `docs/RULE_TAXONOMY.md`: automatic rules' allowed outcomes was missing `cantTell` (4 rules use it as a defensive fallback); the "current intents"/"current families" lists were stale and read as exhaustive when the ruleset actually spans 54 suffixes/68 prefixes
|
|
250
|
+
- `docs/RULE_TAXONOMY.md`: automatic rules' allowed outcomes was missing `cantTell` (4 rules use it as a defensive fallback); the "current intents"/"current families" lists were stale and read as exhaustive when the ruleset actually spans 54 suffixes/68 prefixes, reframed as illustrative with a pointer to the generated `RULE_CATALOG.md`; `data.visibilityFilter.targetSet` was missing the `'dom'` value.
|
|
167
251
|
- `docs/OUTPUT_SCHEMA.md`: the `visibilityFilter` type was missing its always-present `eligible` field; the worked example's `structuralPath` values for the button/img pair were swapped; removed a citation to a note in `RULE_AUTHORING.md` that doesn't exist there.
|
|
168
|
-
- `docs/I18N.md`: stale key counts (`en` listed as 590, actual 600; `fr` coverage listed as ~49%, actual ~48% at the time)
|
|
252
|
+
- `docs/I18N.md`: stale key counts (`en` listed as 590, actual 600; `fr` coverage listed as ~49%, actual ~48% at the time), now updated to reflect full parity.
|
|
169
253
|
|
|
170
254
|
## [1.0.1] - 2026-07-28
|
|
171
255
|
|
|
172
256
|
### Changed
|
|
173
|
-
- Trimmed the published npm package: rule-authoring scaffolding (`docs/RULE_TEMPLATE.*`, `docs/RULE_TEST_TEMPLATE.md`, `docs/RULE_TEST_AUTHORING.md`, `docs/TEST_OUTCOME_STABILITY.md`) and the not-yet-documented `src/explain` module no longer ship in the tarball
|
|
257
|
+
- Trimmed the published npm package: rule-authoring scaffolding (`docs/RULE_TEMPLATE.*`, `docs/RULE_TEST_TEMPLATE.md`, `docs/RULE_TEST_AUTHORING.md`, `docs/TEST_OUTCOME_STABILITY.md`) and the not-yet-documented `src/explain` module no longer ship in the tarball; both stay in the git repo for contributors.
|
|
174
258
|
- README rewritten for clarity: corrected the install command and `require()` examples to the actual package name (`@surea11y/core`), and updated the rule count to 125.
|
|
175
|
-
- `docs/ENGINE_OPTIONS.md`: documented the previously-undocumented `visibilityMode` option (`'styleOnly'`/`'styleAndGeometry'`, scoped to the three contrast rules), and added a "Recipes" section with composed, runnable examples for common scenarios
|
|
259
|
+
- `docs/ENGINE_OPTIONS.md`: documented the previously-undocumented `visibilityMode` option (`'styleOnly'`/`'styleAndGeometry'`, scoped to the three contrast rules), and added a "Recipes" section with composed, runnable examples for common scenarios.
|
|
176
260
|
|
|
177
261
|
### Fixed
|
|
178
|
-
- `aria-allowed-attr`'s `SUPPORTED_ATTRS_BY_ROLE` table reconciled against the published WAI-ARIA 1.2 Recommendation
|
|
179
|
-
- README: a "Real browser execution" code sample passed four positional arguments to `page.evaluate()` and claimed it worked with "any" automation framework
|
|
262
|
+
- `aria-allowed-attr`'s `SUPPORTED_ATTRS_BY_ROLE` table reconciled against the published WAI-ARIA 1.2 Recommendation, since a number of the previous `aria-expanded` allowances turned out to be ARIA 1.1 legacy carryovers rather than current-spec facts. Added `aria-expanded` to 10 roles and `aria-activedescendant` to 8 composite-widget roles, plus smaller posinset/setsize/readonly/required/level gaps; removed `tree`'s unverified `aria-readonly`.
|
|
263
|
+
- README: a "Real browser execution" code sample passed four positional arguments to `page.evaluate()` and claimed it worked with "any" automation framework, but Playwright's `page.evaluate()` only accepts one argument alongside the function and throws on this pattern. Now shown as Puppeteer-specific, with a pointer to `INTEGRATION.md`'s wrapper for Playwright.
|
|
180
264
|
- README: the JSON output example referenced a nonexistent rule id (`link-name-quality`); corrected to the real id, `link-name-quality-manual`.
|
|
181
|
-
- README: Quick Start code samples labeled the runner's four positional arguments as `url, ruleFilter, options, policy`; corrected to the actual names (`pageUrl, contextSelector, engineOptions, runOnly`)
|
|
265
|
+
- README: Quick Start code samples labeled the runner's four positional arguments as `url, ruleFilter, options, policy`; corrected to the actual names (`pageUrl, contextSelector, engineOptions, runOnly`).
|
|
182
266
|
|
|
183
267
|
## [1.0.0] - 2026-07-26
|
|
184
268
|
|
|
185
269
|
### Added
|
|
186
|
-
- 125 rules (77 automatic/`fail`-capable, 48 manual/advisory)
|
|
187
|
-
- Full i18n support (English complete, French partial
|
|
270
|
+
- 125 rules (77 automatic/`fail`-capable, 48 manual/advisory). See `docs/RULE_CATALOG.md` for the full list.
|
|
271
|
+
- Full i18n support (English complete, French partial). See `docs/I18N.md`.
|
|
188
272
|
- A generated public rule catalog (`docs/RULE_CATALOG.md`, via `npm run docs:rule-catalog`) and WCAG facet-coverage report (`coverage/coverage-report.md`, via `npm run coverage`).
|
|
189
273
|
- This documentation set: `README.md`, `docs/OUTPUT_SCHEMA.md`, `docs/ENGINE_OPTIONS.md`, `docs/WCAG_CONFORMANCE.md`, `docs/POLICY.md`, `docs/I18N.md`, `docs/INTEGRATION.md`, `docs/LIMITATIONS.md`, `docs/TROUBLESHOOTING.md`, `LICENSE`.
|
|
190
|
-
- Multi-region `contextSelector` support: pass an array of selectors (or one comma-separated selector string) to scan multiple, possibly disjoint regions in a single run
|
|
274
|
+
- Multi-region `contextSelector` support: pass an array of selectors (or one comma-separated selector string) to scan multiple, possibly disjoint regions in a single run. Overlapping/nested regions are deduped automatically. See `docs/ENGINE_OPTIONS.md`.
|
|
191
275
|
- `includeShadowDom` now defaults to `true` (opt out with `includeShadowDom: false`).
|
|
192
|
-
- `structuralPath` on every `fail`/`cantTell` occurrence: a sibling-index path from `documentElement` down to the flagged element, a more robust element-identity mechanism than `selector` alone
|
|
193
|
-
- `engineOptions.customRules`: register additional rules at runtime, scan-scoped
|
|
194
|
-
- `runa11yCoreAcrossFrames` / `a11yCoreEnableFrameResponder`: cross-frame (including
|
|
195
|
-
- `runOnly.tags` filtering by WCAG version: every rule/composite now carries the version-correct `wcag2*`/`wcag21*`/`wcag22*` level tag for its Success Criterion
|
|
196
|
-
- `docs/BINDING_AUTHORS_GUIDE.md`: a reference for building a
|
|
276
|
+
- `structuralPath` on every `fail`/`cantTell` occurrence: a sibling-index path from `documentElement` down to the flagged element, a more robust element-identity mechanism than `selector` alone. See `docs/OUTPUT_SCHEMA.md`.
|
|
277
|
+
- `engineOptions.customRules`: register additional rules at runtime, scan-scoped, matching the shape of an internal rule module. `runInPage`/`applicability` accept a real function or a function-source string, the latter needed for cross-realm callers whose `engineOptions` argument can't carry a live function across a serialization boundary. See `docs/ENGINE_OPTIONS.md`.
|
|
278
|
+
- `runa11yCoreAcrossFrames` / `a11yCoreEnableFrameResponder`: cross-frame (including cross-origin) scanning for the plain-script-injection consumption mode, a cooperative `postMessage` protocol. See `docs/INTEGRATION.md`'s "Cross-frame scanning" section and `docs/OUTPUT_SCHEMA.md`'s "Cross-frame result" section.
|
|
279
|
+
- `runOnly.tags` filtering by WCAG version: every rule/composite now carries the version-correct `wcag2*`/`wcag21*`/`wcag22*` level tag for its Success Criterion, so a caller can select a WCAG 2.0/2.1/2.2 conformance target by combining tag sets. See `docs/ENGINE_OPTIONS.md`'s "Filtering by WCAG version" section and `src/coverage/wcag-version-map.js`.
|
|
280
|
+
- `docs/BINDING_AUTHORS_GUIDE.md`: a reference for building a new framework binding on top of this engine, what's already engine-level vs. what every binding has to build itself.
|
|
197
281
|
|
|
198
282
|
### Fixed (selected)
|
|
199
|
-
- A shared `buildSelector` helper bug where an ancestor element that was the
|
|
200
|
-
- Several `ALLOWED_ROLES_BY_ELEMENT` entries (`<label>`, `<table>`/`<td>`/`<th>`/`<tr>`, `input[type=checkbox][role=button]`) that were missing or too restrictive
|
|
283
|
+
- A shared `buildSelector` helper bug where an ancestor element that was the last of several same-tag siblings got no `:nth-of-type()` disambiguation, producing selectors that matched multiple elements.
|
|
284
|
+
- Several `ALLOWED_ROLES_BY_ELEMENT` entries (`<label>`, `<table>`/`<td>`/`<th>`/`<tr>`, `input[type=checkbox][role=button]`) that were missing or too restrictive.
|
|
201
285
|
- `aria-hidden-focus` false-flagging the common `tabindex="-1"`-behind-`aria-hidden` pattern (checked raw focusability instead of tabbability).
|
|
202
286
|
- A label-naming bug (`hasLabelAssociation` ignoring a `<label>`'s own `aria-label`), duplicated across 7 rule files, all fixed identically.
|
|
203
287
|
- A systemic "name from content" false-positive affecting 19 rule files (an `<img alt>` or `aria-label`-named descendant inside a link/button wasn't recognized as providing the accessible name).
|
|
204
|
-
- `contextSelector` resolving via `document.querySelector` (first match only) instead of `querySelectorAll
|
|
205
|
-
- Three rules (`form-control-programmatic-label-present`, `target-size-minimum`, `label-in-name`) that queried `ctx.root` directly instead of through the shared `queryAllSmart`/`queryAll` helpers,
|
|
206
|
-
- `aria-required-parent`/`aria-required-children`'s ancestor/descendant searches and `getContentNameInfo`'s
|
|
207
|
-
- `landmark-one-main` incorrectly also flagged "more than one main landmark"
|
|
208
|
-
- `aria-required-children`, `aria-prohibited-children`, `aria-required-parent`, and `aria-required-attr` flagged containers/elements
|
|
209
|
-
- `getAccessibleLandmarkName`, duplicated across 7 landmark rule files, never checked an element's `title` attribute as a naming source
|
|
288
|
+
- `contextSelector` resolving via `document.querySelector` (first match only) instead of `querySelectorAll`, so a selector matching several elements silently scanned only the first.
|
|
289
|
+
- Three rules (`form-control-programmatic-label-present`, `target-size-minimum`, `label-in-name`) that queried `ctx.root` directly instead of through the shared `queryAllSmart`/`queryAll` helpers, silently broken the moment `ctx.root` became an array.
|
|
290
|
+
- `aria-required-parent`/`aria-required-children`'s ancestor/descendant searches and `getContentNameInfo`'s name-from-content walk not following shadow-DOM `<slot>` assignment; a duplicated `resolveAriaLabelledbyText` pattern across 16 rules and `getLabelText` across 7 not checking an `aria-labelledby`/`<label>` target's `title` attribute as a final accname fallback.
|
|
291
|
+
- `landmark-one-main` incorrectly also flagged "more than one main landmark", out of its real scope (duplicates are `landmark-no-duplicate-main`'s job) and missing the accessibility-tree visibility filter its sibling rule already has.
|
|
292
|
+
- `aria-required-children`, `aria-prohibited-children`, `aria-required-parent`, and `aria-required-attr` flagged containers/elements not currently exposed to the accessibility tree at all. All four now skip elements that fail `isAccTreeEligible`; `aria-required-children`/`aria-required-attr` also honor `aria-busy="true"` as an explicit author signal of transient incompleteness.
|
|
293
|
+
- `getAccessibleLandmarkName`, duplicated across 7 landmark rule files, never checked an element's `title` attribute as a naming source. Replaced all 7 copies with one shared helper, `helpers.getLandmarkNameInfo`.
|
|
210
294
|
|
|
211
295
|
### Known limitations
|
|
212
|
-
See `docs/LIMITATIONS.md
|
|
296
|
+
See `docs/LIMITATIONS.md`: structural (keyboard-trap detection, reflow-at-zoom), environment-dependent (jsdom vs. real-browser geometry), and deliberately-not-automated (text-quality judgment calls) limitations, stated explicitly rather than left to be discovered.
|
|
213
297
|
|
|
214
298
|
---
|
|
215
299
|
|
|
216
300
|
# How to add an entry
|
|
217
301
|
|
|
218
302
|
When you ship a change worth calling out to consumers (not every commit):
|
|
219
|
-
1. Add a bullet under `[Unreleased]`, in the right subsection (`Added`, `Changed`, `Fixed`, `Deprecated`, `Removed`, `Security`)
|
|
303
|
+
1. Add a bullet under `[Unreleased]`, in the right subsection (`Added`, `Changed`, `Fixed`, `Deprecated`, `Removed`, `Security`), create the subsection if it doesn't exist yet for this cycle.
|
|
220
304
|
2. Write it from the consumer's perspective ("what changed for someone using this package"), not the implementation's.
|
|
221
305
|
3. When you tag a release, rename `[Unreleased]` to `## [x.y.z] - YYYY-MM-DD` and start a fresh empty `[Unreleased]` above it.
|