@surea11y/core 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.md +157 -54
  3. package/docs/ACT_RULE_MAPPING.md +8 -7
  4. package/docs/API_STABILITY.md +18 -5
  5. package/docs/BINDING_AUTHORS_GUIDE.md +2 -2
  6. package/docs/CI_INTEGRATIONS.md +43 -0
  7. package/docs/DESIGN_CHALLENGES.md +97 -3
  8. package/docs/EARL.md +2 -2
  9. package/docs/ENGINE_OPTIONS.md +81 -3
  10. package/docs/I18N.md +62 -20
  11. package/docs/JUNIT.md +73 -0
  12. package/docs/LIMITATIONS.md +1 -0
  13. package/docs/OUTPUT_SCHEMA.md +19 -6
  14. package/docs/REPORT.md +7 -2
  15. package/docs/RULE_AUTHORING.md +73 -6
  16. package/docs/RULE_CATALOG.md +139 -116
  17. package/docs/RULE_EXAMPLES.md +2189 -0
  18. package/docs/RULE_HELPERS.md +62 -5
  19. package/docs/RULE_TAXONOMY.md +2 -2
  20. package/docs/SARIF.md +2 -1
  21. package/docs/WCAG_CONFORMANCE.md +56 -3
  22. package/package.json +34 -11
  23. package/profiles/index.js +14 -0
  24. package/src/checks/automatic/area-alt-present.js +87 -31
  25. package/src/checks/automatic/aria-braille-equivalent.js +25 -7
  26. package/src/checks/automatic/aria-hidden-focus.js +74 -18
  27. package/src/checks/automatic/aria-prohibited-attr.js +17 -4
  28. package/src/checks/automatic/aria-required-attr.js +29 -0
  29. package/src/checks/automatic/aria-role-name-present.js +19 -2
  30. package/src/checks/automatic/aria-valid-attr-value.js +28 -16
  31. package/src/checks/automatic/autocomplete-valid.js +26 -11
  32. package/src/checks/automatic/avoid-inline-spacing.js +105 -40
  33. package/src/checks/automatic/button-name-present.js +2 -1
  34. package/src/checks/automatic/canvas-text-alternative-present.js +105 -14
  35. package/src/checks/automatic/combobox-name-present.js +34 -51
  36. package/src/checks/automatic/contrast-computable.js +35 -4
  37. package/src/checks/automatic/contrast-enhanced.js +4 -4
  38. package/src/checks/automatic/contrast-minimum.js +45 -11
  39. package/src/checks/automatic/css-orientation-lock.js +152 -30
  40. package/src/checks/automatic/definition-list-children-valid.js +67 -23
  41. package/src/checks/automatic/deprecated-elements-not-used.js +43 -38
  42. package/src/checks/automatic/dialog-name-present.js +28 -9
  43. package/src/checks/automatic/duplicate-id.js +6 -2
  44. package/src/checks/automatic/identical-iframes-same-purpose.js +4 -4
  45. package/src/checks/automatic/iframe-focusable-content.js +7 -4
  46. package/src/checks/automatic/iframe-title-unique.js +36 -81
  47. package/src/checks/automatic/input-image-alt-present.js +32 -20
  48. package/src/checks/automatic/label-in-name.js +40 -13
  49. package/src/checks/automatic/language-page-present.js +12 -6
  50. package/src/checks/automatic/link-in-text-block.js +272 -60
  51. package/src/checks/automatic/link-name-present.js +13 -5
  52. package/src/checks/automatic/list-children-valid.js +18 -1
  53. package/src/checks/automatic/listbox-name-present.js +19 -49
  54. package/src/checks/automatic/listitem-parent-valid.js +4 -3
  55. package/src/checks/automatic/meta-refresh-no-exceptions.js +3 -3
  56. package/src/checks/automatic/page-title-present.js +16 -4
  57. package/src/checks/automatic/progressbar-name-present.js +11 -1
  58. package/src/checks/automatic/role-img-text-alternative-present.js +9 -5
  59. package/src/checks/automatic/searchbox-name-present.js +32 -49
  60. package/src/checks/automatic/server-side-image-map-absent.js +48 -28
  61. package/src/checks/automatic/slider-name-present.js +38 -52
  62. package/src/checks/automatic/spinbutton-name-present.js +32 -49
  63. package/src/checks/automatic/target-size-minimum.js +0 -11
  64. package/src/checks/automatic/td-has-header.js +41 -5
  65. package/src/checks/automatic/text-spacing-content-loss.js +548 -0
  66. package/src/checks/automatic/textbox-name-present.js +32 -49
  67. package/src/checks/automatic/valid-lang.js +15 -10
  68. package/src/checks/manual/area-alt-quality-manual.js +113 -31
  69. package/src/checks/manual/canvas-text-alternative-quality-manual.js +0 -2
  70. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +23 -10
  71. package/src/checks/manual/css-hidden-focus.js +215 -7
  72. package/src/checks/manual/form-control-label-quality-manual.js +109 -5
  73. package/src/checks/manual/heading-order-manual.js +9 -1
  74. package/src/checks/manual/heading-quality-manual.js +143 -9
  75. package/src/checks/manual/img-alt-decorative-manual.js +6 -3
  76. package/src/checks/manual/input-image-alt-quality-manual.js +81 -13
  77. package/src/checks/manual/link-name-quality-manual.js +130 -4
  78. package/src/checks/manual/media-transcript-present-manual.js +65 -8
  79. package/src/checks/manual/mouse-only-event-handlers-manual.js +66 -5
  80. package/src/checks/manual/no-autoplay-audio-manual.js +93 -6
  81. package/src/checks/manual/p-as-heading-manual.js +89 -44
  82. package/src/checks/manual/page-title-patterns-manual.js +77 -8
  83. package/src/checks/manual/skip-link-manual.js +42 -14
  84. package/src/checks/manual/table-fake-caption-manual.js +32 -1
  85. package/src/checks/manual/video-caption-manual.js +47 -24
  86. package/src/checks/manual-review.js +0 -4
  87. package/src/core.js +14285 -2219
  88. package/src/coverage/en301549-map.js +187 -0
  89. package/src/coverage/standards.js +279 -0
  90. package/src/coverage/wcag-facets.js +1119 -0
  91. package/src/coverage/wcag-version-map.js +101 -0
  92. package/src/en301549.js +33 -0
  93. package/src/junit.js +321 -0
  94. package/src/profile-kit.js +163 -0
  95. package/src/report.js +343 -74
  96. package/src/sarif.js +34 -3
  97. package/src/wcag.js +105 -0
  98. package/surea11y.browser.js +5 -4
  99. package/surea11y.i18n.de.js +1 -1
  100. package/surea11y.i18n.es.js +1 -1
  101. package/surea11y.i18n.fr.js +1 -1
  102. package/surea11y.i18n.ja.js +3 -0
  103. package/src/checks/manual/area-alt-decorative-manual.js +0 -255
package/CHANGELOG.md CHANGED
@@ -4,6 +4,108 @@ All notable changes to this project are documented here, in [Keep a Changelog](h
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.8.1] - 2026-10-02
8
+
9
+ ### Added
10
+ - CI runs ACT's test-case corpus on every push and pull request, and the release workflow runs it against the live corpus before publishing; either fails on a false positive, or when the corpus cannot be fetched in full. The false positive fixed below reached 1.8.0 because the corpus only ran after the release was tagged.
11
+ - ACT rule 4b1c6c is mapped to `identical-iframes-same-purpose`, so the implementation report covers it. The rule reports `cantTell`, never `fail`, for frames sharing a name but embedding different resources, so the mapping is partial.
12
+
13
+ ### Fixed
14
+ - `autocomplete-valid` no longer fails a well-formed field name that does not suit its control, such as `bday-day` on `<input type="tel">`, which 1.8.0 began failing. The name still identifies the input's purpose, which is all WCAG 1.3.5 asks, and ACT rule 73f2c2 passes this exact case (Passed Example 8), so 1.8.0 reported a false positive in the ACT implementation report. Its reason code `AUTOCOMPLETE_FIELD_CONTROL_MISMATCH` is retired with the failure it named, so a stored baseline entry or Code Scanning alert for it closes. The rule's other 1.8.0 fixes stay: `impp` and the `tel-local-prefix`/`tel-local-suffix` field names, and controls disabled by a disabled `<fieldset>`.
15
+
16
+ ## [1.8.0] - 2026-10-02
17
+
18
+ ### Added
19
+ - Japanese (`ja`) is a shipped locale: `src/i18n/ja.json` translates every key `en.json` has, `{ locale: 'ja' }` and `ja-JP` resolve to it with `engine.locale.reason` `ok` and `primary-subtag`, and the build emits `surea11y.i18n.ja.js` for the standalone bundle, reachable as `@surea11y/core/i18n/ja` through the existing `./i18n/*` export. WCAG terms follow WAIC's Japanese translation of WCAG 2.2; `docs/I18N.md` lists the conventions the file keeps. `ja` was the documentation's example of a locale that falls back to English, so that example is now `ko`.
20
+ - When the HTML report's own labels are in another language than the scan (a dictionary supplied at scan time, which the report cannot use), rule titles, summaries and hints translated by that dictionary carry `lang` with the scan's locale, so a screen reader reads them with the right pronunciation. Text that fell back to English is left unmarked.
21
+ - A test holds every locale file to the same `{{placeholder}}` set as `en.json`, so a translation cannot drop a value silently.
22
+ - `@surea11y/core/junit` renders a scan result as JUnit XML for the test views of GitLab, Azure DevOps, Jenkins and CircleCI. `renderJunitReport(result, options)` makes one `<testsuite>` per WCAG Success Criterion and one `<testcase>` per rule mapped to it, so a defect repeated across a page is one failing test listing every occurrence, and counts stay stable between runs. Suites come from each rule's own WCAG mappings rather than the composites, so every rule that ran is reported even when composites were excluded; rules mapped to no criterion go into a final `Other checks` suite. `fail` becomes `<failure>`, `cantTell` becomes `<skipped>` (JUnit has no "could not tell", and skipped surfaces it without failing a build, as SARIF's `warning` does), `pass` is a bare testcase, and `notApplicable` is left out unless `includeNotApplicable` is set. `cantTellAs: 'failure'` gates on undecided rules instead. `baselineEntries` drops known failures exactly as SARIF does, and a rule whose every failure is known is skipped with the count rather than reported as passing. Each suite's properties carry the criterion, its level, the EN 301 549 clause, the composite's outcome, and the run's engine, WCAG version, profile, locale and URL. Every `time` is `"0"` and `timestamp` appears only when the result has one, so the same scan renders byte-identical XML. See [`docs/JUNIT.md`](./docs/JUNIT.md); [`docs/CI_INTEGRATIONS.md`](./docs/CI_INTEGRATIONS.md#junit-test-reports) has GitLab and Azure DevOps recipes.
23
+ - `engineOptions.profile` names a conformance target instead of a hand-written tag set: `wcag22-aa`, `en301549-v4.1.1` (both WCAG 2.2 A and AA), `en301549-v3.2.1` (WCAG 2.1 A and AA) and `section508` (WCAG 2.0 A and AA). Level tags do not nest, so a correct A + AA target meant listing four or six tags; a profile supplies them, and the WCAG target version follows from them as it already did, which is what lets `duplicate-id` still fail under `en301549-v3.2.1`, where 4.1.1 Parsing still exists, and not under `en301549-v4.1.1`. An include in `runOnly` or in `engineOptions.rules`/`.tags`/`.tests` still selects the rules instead; excludes apply on top of the profile from either route, so a binding's `disableTags()` narrows it rather than replacing it; and an explicit `wcagVersion` still wins. The run reports the profile it used as `engine.profile`; one that did not take effect, an unknown name or an overridden one, is ignored like other invalid option values, but logs a `console.warn` and leaves `engine.profile` absent, since a caller who asked for a conformance target and silently got a full run would misread the result. A profile selects rules; it is not a claim that passing them meets the named standard. The HTML report's meta bar and the SARIF run's `properties` show the WCAG target and profile a scan used. See [`docs/ENGINE_OPTIONS.md`](./docs/ENGINE_OPTIONS.md#conformance-profiles).
24
+ - A scan can name, on every atomic and composite result, the EN 301 549 clause that restates each of its WCAG Success Criteria. It does so only when asked: `engineOptions.mappings` takes `'en301549'` for both versions or `'en301549:V3.2.1'` / `'en301549:V4.1.1'` for one, an EN 301 549 profile switches on the version it targets, and the run reports what it named as `engine.mappings`. By default a result names WCAG only, since a clause of a standard the caller does not audit against is noise in every report; an unknown name or version is ignored with a console warning, and a `customRules` rule keeps exactly the mappings it declares. When asked for, after its WCAG entries, `meta.normativeMappings` carries `{ standard: "EN 301 549", version, requirement: "9.1.1.1", title, wcagSc: ["1.1.1"] }` once per version of the standard that includes the criterion: V3.2.1 (2021-03, built on WCAG 2.1) and V4.1.1 (2026-09, built on WCAG 2.2). The two restate different criteria, so a 2.5.8 result carries only V4.1.1, a 4.1.1 result only V3.2.1, and an AAA result neither. The table is `src/coverage/en301549-map.js`, taken from the ETSI text and held by `tests/coverage/en301549-map.test.js` to exactly the Level A and AA criteria of each version's WCAG version in the engine's own registry: 50 for V3.2.1, 55 for V4.1.1. Titles are each version's own wording. Rules do not declare these entries; the build derives them from each rule's WCAG mapping, so `manual-review` drops the three V3.2.1 entries it carried by hand. SARIF adds an `en301549-<clause>` tag per clause (one tag where both versions share it), and the HTML report's WCAG rollup names the clause under each criterion. `normativeMappings` was already not WCAG-only, and with EN 301 549 switched on it carries other-standard entries on nearly every result: a consumer that reads every entry's `requirement` as a WCAG criterion must filter on `standard` first, as SARIF, EARL and the HTML report do. `wcagSc` is unchanged. The catalogs name the same clauses under the same options: `getChecksCatalog(engineOptions)` in `normativeMappings`, and composite entries from `getRulesCatalog(engineOptions)` as `meta.standardMappings`. `wcagSc` on each entry names the criterion it restates, so JUnit files a clause under the right suite even for a rule mapped to several criteria. EN 301 549 is registered in `src/coverage/standards.js`, which the build, `engineOptions.mappings`, the profiles and the SARIF, JUnit and HTML reporters all read, so another standard is a table and a registry entry (see [`docs/WCAG_CONFORMANCE.md`](./docs/WCAG_CONFORMANCE.md#adding-another-standard)). The table itself is public as `@surea11y/core/en301549` (`EN301549_VERSIONS`, `EN301549_CLAUSES`, `en301549ClausesForSc()`), frozen, for tools that need to know which criteria a version requires. See [`docs/WCAG_CONFORMANCE.md`](./docs/WCAG_CONFORMANCE.md#en-301-549).
25
+ - A registered standard can bring rollups of its own (`composites()` in its registry entry), which a run under its profile adds to `rulesResults` next to the WCAG ones. Each carries the standard's wording as its title, `meta.standard` and `data.details.standard`/`version`/`criterion`, and decides its outcome the same way as a WCAG rollup. They are opt-in like the standard's own rules: default, WCAG and EN 301 549 runs do not produce them, and `getRulesCatalog()` lists them only under options that ask for the standard. The HTML report shows them in their own section after the WCAG rollup. Every check result now also carries `rollupIds`, the rollups that group it in that run, empty when there are none, so a finding no WCAG rollup groups is not lost to a consumer reading `rulesResults` alone.
26
+ - Opt-in rules. A rule tagged with a standard's rule tag (from `ruleTag` in `src/coverage/standards.js`) checks a requirement only that standard makes, so it is off by default: it runs only through that standard's profile, a selection that includes the tag, its own id or the id of its standard's rollup that groups it, never in a default run, a WCAG tag set or a WCAG or EN 301 549 profile. Excludes still apply. A WCAG scan therefore never fails a page for something WCAG does not require, and `docs/WCAG_CONFORMANCE.md` now states the promise as "a violation of the standard and version you targeted". A standard's profile can also run every rule the standard maps (`mappedRules` in the registry), computed at build time. See [`docs/ENGINE_OPTIONS.md`](./docs/ENGINE_OPTIONS.md#opt-in-rules).
27
+ - `engineOptions.optInRules` runs opt-in rules outside their standard's profile: `'all'` for every standard's, or a list of rule tags. It only lifts the opt-in gate, so the rest of the selection still decides: with no profile and no filter, `{ optInRules: 'all' }` runs every rule the engine has and every standard's rollups, while under a WCAG profile it runs the WCAG rules only and under a standard's own profile it changes nothing for that standard's rules. Excludes apply as usual, and a tag that is no opt-in tag is ignored with a console warning. It is meant for seeing everything the engine can report, not as a conformance target, so the result says when it added rules: `engine.optInRules` lists the tags whose rules it added, and the HTML report's meta bar, SARIF's run properties and JUnit's suite properties show it. It adds no other standard's numbers to results; `mappings` still does that. `getRulesCatalog(engineOptions)` and `getChecksForRunOnly()` follow the option the same way. See [`docs/ENGINE_OPTIONS.md`](./docs/ENGINE_OPTIONS.md#running-every-rule-optinrules).
28
+ - `text-spacing-content-loss` checks WCAG 1.4.12 in a browser. It applies line height 1.5, 2em after paragraphs, letter spacing 0.12em and word spacing 0.16em in a style sheet that wins over the page's own, and fails text that a box with `overflow: hidden` or `clip` then cuts off by at least half a line. It asks about text pushed out by less and text that comes to overlap other text. In any environment it asks about a style sheet rule that forces line-height, letter-spacing or word-spacing below those values with `!important`: a tool that adds its own style sheet cannot override it, though a user style sheet can. In jsdom without such a rule it is `notApplicable`. It runs by default, so in a browser the `wcag-1.4.12-text-spacing` rollup can now fail or ask where only `avoid-inline-spacing` spoke before. Text that was not visible before the spacing (visually hidden text, text hidden by opacity or visibility, text outside its clipping box) is left out, and a box that scrolls keeps what goes past it reachable, so only text a person could read and then loses is reported.
29
+ - Helpers for rules, documented in [`docs/RULE_HELPERS.md`](./docs/RULE_HELPERS.md): `queryAllSource(selector)`, the same query as `queryAllSmart()` (shadow roots, scope, `excludeSelectors`) without the hidden-content filter, for checks that apply to the whole source; `getDoctypeInfo()`, the doctype a page has; `hasSkipLinkWording(text)`, whether a link's text reads as a skip link in the languages the engine ships, one list for every rule that looks for a skip link; and an optional bold-text threshold for `contrast.isLargeText()`, which keeps WCAG's when left out.
30
+ - Profiles: a standard with verdicts of its own lives in its own folder under `profiles/`, holding its entry, tables, rules, messages and the languages it offers, tests, scenario pages, docs and records. The engine reaches it only through `profiles/index.js`, a profile uses only what core publishes, and core's own tests never read a profile's files, which `tests/profile-boundary.test.js` checks both ways; a profile never changes what a rule decides, only which rules run (`tests/profile-outcomes.test.js`). No standard ships as a profile in this release. `npm run profile:new -- <key> --name "<Name>"` creates one that builds and passes its tests as it is; filling it in is editing two tables and adding rules. Core's tests of what it does with a profile's standard (mappings, opt-in rules, rollups, variants, `ctx.standard`, JUnit and report output) run against a sample profile, `tests/fixtures/profiles/sample/`, built into a copy of the engine. See [`profiles/README.md`](./profiles/README.md).
31
+ - `@surea11y/core/wcag` publishes WCAG's Success Criteria as each version of WCAG 2 publishes them: `wcagCriteria(version, { levels })`, `wcagCriterion(sc, version)`, and `wcagTags(version, levels)` for the rule tags that select a version's criteria. 4.1.1 Parsing is a Level A criterion in 2.0 and 2.1 and absent from 2.2, where the engine's own table, written for 2.2, gives it no level. EN 301 549's profiles take their tags from it.
32
+ - A rule reads which standard and version the run targets as `ctx.standard` (`{ key, name, version }`, or `null` when no standard's profile selected the run), for a requirement that differs between versions of a standard. See [`docs/RULE_AUTHORING.md`](./docs/RULE_AUTHORING.md).
33
+ - A standard's profile can leave rules and WCAG criteria out (`exclude: { rules, criteria }` in its registry entry), for a standard that waives a criterion or replaces a WCAG check with its own. Those rules and their WCAG rollups do not run, the rule catalog leaves them out too, and the result names what was left out as `engine.profileExcludes`. No built-in profile excludes anything.
34
+ - Rule variants: a rule that is a core rule with other thresholds is declared as data (`from`, `config`) rather than copied, and runs the base rule's code with its own id, settings and messages. `contrast-minimum` declares its thresholds as `settings`; they are set by variants only, never by a scan's options. See [`docs/RULE_AUTHORING.md`](./docs/RULE_AUTHORING.md#rule-variants).
35
+ - The finding identities a release ships are frozen in `scripts/data/released-finding-ids.json` (`npm run finding-ids:release -- <version>`, as part of each release), and `tests/released-finding-ids.test.js` fails when one is missing from a later commit without a recorded retirement and its reason. The inventory test alone compared against a file the same commit could regenerate. `docs/API_STABILITY.md` states when a reason code may retire: only with the finding it named, through a correctness fix.
36
+
37
+ ### Changed
38
+ - `p-as-heading` also asks about a `<div>` that holds only text and inline markup, and reads the weight and size of each piece of text, so a paragraph made bold by a styled `<span>` is asked about and one with a word in normal weight is not. Text inside a heading, button, label, legend, caption, table header or `<summary>`, and a `<div>` with a role, are left out. It can return `cantTell` on pages where it was `notApplicable` before, under WCAG 1.3.1.
39
+ - `no-autoplay-audio` also asks about `<embed>` and `<object>` elements that load sound, video or a plugin, and about `<bgsound>`, unless `autostart` or `autoplay` is set to false; the fallback inside an `<object>` it already asks about is not asked about again. WCAG 1.4.2 covers any sound that plays on its own. It can return `cantTell` on pages where it was `notApplicable` before.
40
+ - `link-name-present` maps to WCAG 2.4.4 Link Purpose (In Context) as well as 4.1.2, as ACT rule `c487ae` does: a link with no accessible name has no purpose to determine. It joins the `wcag-2.4.4-link-purpose-in-context` composite, which until now rolled up only the manual `link-name-quality` and so could never fail; an unnamed link now fails it. The rule's results also name EN 301 549 9.2.4.4 when asked for.
41
+ - `area-alt-present` fails an `<area>` with `alt=""` and no other accessible name, instead of treating an empty `alt` as satisfying the check. An `<area>` in a used image map is always a link — it has no visual content of its own the way an `<img>` does — so `alt=""` never carried `<img alt="">`'s decorative meaning; it just left the link's name empty. `aria-label`/`aria-labelledby` and a non-empty `title` still count as valid naming mechanisms, as they did before. The empty-`alt` case gets its own message (`area_altPresent_summary_fail_empty`/`_hint_fail_empty`), distinct from the missing-`alt` one, in every shipped locale.
42
+ - `manual-review` no longer names EN 301 549 clauses by default. It carried three V3.2.1 entries by hand; they now come, for both versions, from its WCAG mapping when EN 301 549 is asked for (`engineOptions.mappings` or an EN 301 549 profile).
43
+ - `en.json` has new keys for rollup titles (`catalog.rules.*`) and the HTML report (`report_*`). A caller dictionary written for 1.7.0 still works, and those strings fall back to English, but `engine.locale.reason` now reads `partial-dictionary` until it adds them.
44
+
45
+ ### Deprecated
46
+ - `iframe-title-unique` is deprecated in favour of `identical-iframes-same-purpose`, and reports `notApplicable` on every page. It failed two frames for sharing a `title`, which WCAG 4.1.2 does not require: the criterion asks that a frame's name be exposed, not unique, and ACT rule 4b1c6c accepts the same name on frames that embed the same resource, so seven of its ten passed examples failed. Whether frames sharing a name embed the same resource is what `identical-iframes-same-purpose` checks. A deprecated rule normally keeps running, so this one was also reduced to `notApplicable`, to stop the false failure now rather than when the file is removed in 2.0.0. Its id stays in the catalog with `deprecated: true` and `deprecation.replacedBy`, the first rule to use the mechanism in `docs/API_STABILITY.md`. `IFRAME_TITLE_DUPLICATE` is retired with the finding it named, so a stored baseline entry or Code Scanning alert for it closes. Its scenario page and message keys are removed. (#16, #17)
47
+
48
+ ### Removed
49
+ - `area-alt-decorative`, the manual-review companion that asked a human to confirm an empty-`alt` `<area>` was decorative. That question never had a legitimate "yes" — `area-alt-present` now fails the case outright (see above) — so its fixture and tests are removed with it. Anything holding its id, a `runOnly` list or a baseline entry, matches nothing now. It was removed without the deprecation period the policy asks for, an exception recorded in [`docs/API_STABILITY.md`](./docs/API_STABILITY.md#a-removal-that-predates-this-guard).
50
+
51
+ ### Fixed
52
+ - `autocomplete-valid` accepts `impp` on its own and the `tel-local-prefix` and `tel-local-suffix` field names. It read `impp` as a prefix such as `home` or `work`, which HTML does not, so `autocomplete="impp"` failed.
53
+ - `tests/fixtures/INDEX.md` finds the scenario page of a rule whose test names it from the rule id (`` `${RULE_ID}-all-scenarios.html` ``). Twenty rules with a page were listed as having none.
54
+ - `identical-iframes-same-purpose` has a scenario page, so every rule that decides something now has one.
55
+ - `deprecated-elements-not-used` no longer fails `<blink>`, which no browser makes blink, and asks instead of failing on `<marquee>`, since the page may offer its own pause control. Its reason code `DEPRECATED_NON_STOPPABLE_ELEMENT` is retired with the failure it named: a `<marquee>` is now reported as `MARQUEE_PAUSE_MECHANISM_UNKNOWN` (`cantTell`), and a `<blink>` not at all, so a stored baseline entry or Code Scanning alert for either closes rather than matching the new finding.
56
+ - `area-alt-quality` and `input-image-alt-quality` ask about an `<area>` or image button named only by `aria-label`, `aria-labelledby` or `title`, not only by `alt`, and list every source they found.
57
+ - `input-image-alt-present` asks instead of failing when the author's name for an image button equals a browser default such as "Submit".
58
+ - `canvas-text-alternative-present` fails a `<canvas role="img">` named only by its fallback content, and passes a canvas with `role="presentation"` or `"none"`.
59
+ - `img-alt-decorative` no longer treats `alt=" "` as the decorative marker, which contradicted `img-alt-present`'s failure on it.
60
+ - `no-autoplay-audio` and `media-alternative-transcript-evidence` report `<audio>` without `controls` in real browsers. The browser's own stylesheet hides it, so both rules skipped it; hidden media still plays.
61
+ - `server-side-image-map-absent` asks instead of failing, since the map's destinations may also be offered as links, and ignores `ismap` on an image outside a link, where it does nothing.
62
+ - `table-fake-caption` no longer asks about layout tables or tables already named by `aria-label`, `aria-labelledby` or `title`.
63
+ - `td-has-header` accepts `role="columnheader"`/`"rowheader"` cells as headers and no longer fails layout tables or empty cells such as a table's corner cell.
64
+ - `video-caption` asks about a video whose only text track is `kind="subtitles"`, which may be a translation rather than captions.
65
+ - `autocomplete-valid` fails a field name that does not suit the control, such as `street-address` on a single-line input or `email` on a number input (`AUTOCOMPLETE_FIELD_CONTROL_MISMATCH`), and no longer fails a control disabled by a disabled `<fieldset>`.
66
+ - `definition-list-children-valid` fails a `<dd>` before the first `<dt>`, a `<dt>` after the last `<dd>` (`DL_DT_DD_ORDER`), and text directly inside the `<dl>`.
67
+ - `dialog-name-present` checks open native `<dialog>` elements and resolves `role="alertdialog dialog"` to its first valid role.
68
+ - `duplicate-id` compares ids exactly as written, so `id="a "` and `id="a"` are no longer duplicates.
69
+ - `heading-order` uses a valid `aria-level` on `<h1>` to `<h6>`.
70
+ - `html-lang-attr-present` and `valid-lang` judge only the primary language subtag, so `fr-FR-!!` and `en-US_x` pass.
71
+ - `label-in-name` compares accented words whole: `poser` is no longer found inside `Déposer`, and `Deposer` does not match `Déposer`.
72
+ - `label-in-name` no longer fails a control whose `aria-labelledby` points at no element or only at elements with no text, or whose `aria-label` is empty once trimmed. The accessible name computation skips such an attribute and takes the name from the next source, so `<button aria-labelledby="missing">Save</button>` is named "Save"; the rule took the name as empty and reported that "Save" was missing from it. Such a control is not named by either attribute and is now out of the rule's scope. The dangling reference is still reported by `aria-valid-attr-value`.
73
+ - `list-children-valid` no longer fails a `<ul>` or `<ol>` given another role such as `tree`, `listbox` or `menubar`, and `listitem-parent-valid` accepts `<li>` inside `<menu>`.
74
+ - `avoid-inline-spacing` asks instead of failing when the text may never wrap: a single word, or text short enough to fit on one line at 320 CSS pixels (`INLINE_SPACING_SHORT_TEXT`).
75
+ - `css-focus-indicator-suppressed` also asks about an outline removed by a rule with no focus state, such as `a { outline: none }`, when no focus rule draws a replacement (F78); such pages were notApplicable.
76
+ - `css-hidden-focus` no longer asks about an off-screen or clipped element, such as a skip link, that a `:focus`, `:focus-visible` or `:not(:focus)` rule brings back into view.
77
+ - `css-orientation-lock` asks when an orientation media query hides the page's main content (a "rotate your device" message, F100); such pages passed.
78
+ - `link-in-text-block` asks instead of passing when a link differs from the surrounding text only by a colour of 3:1 or more, since G183 also needs a cue on hover and focus; it passes links marked by a border, shadow, outline, background, icon or generated content instead of failing them, and checks `role="link"` elements.
79
+ - `mouse-only-event-handlers` asks about an element whose `onfocus`/`onblur` or key handlers can never run, because neither it nor, for key handlers, a descendant can take focus.
80
+ - `page-title-patterns` reviews a `<title>` the parser left in `<body>`, which it skipped, and `page-title-present` reports a missing title, not an empty one, when the only `<title>` on the page is inside an inline `<svg>`.
81
+ - `skip-link` recognises skip links worded in French, German, Spanish and Japanese, and the page's first link when it points into the page before `main`.
82
+ - `aria-hidden-focus` no longer fails a control that an ancestor `<fieldset disabled>` disables, and judges an `<area href>` by the image that uses its map: an aria-hidden area in a used map fails, one in a map no image uses is not reported.
83
+ - `aria-required-attr` no longer asks for `aria-checked` on a native checkbox or radio given a checkable role such as `switch`: the native checked state supplies it.
84
+ - `aria-valid-attr-value` fails integers below the range WAI-ARIA sets, such as `aria-level="0"` or `aria-setsize="-2"`, and asks instead of failing when an `aria-labelledby`, `aria-describedby` or other ID list points only at ids that do not exist; a missing `aria-activedescendant` target still fails.
85
+ - `aria-allowed-role` asks about a role other than `main` on `<main>`, and about foreign roles on the rows and cells of a native table.
86
+ - `aria-prohibited-attr` fails `aria-label` or `aria-labelledby` on a native `<caption>`, as it does on `role="caption"`.
87
+ - `aria-role-name-present` accepts the name a fieldset's `<legend>`, a table's `<caption>` or a `<label>` on `<progress>` or `<meter>` gives the element.
88
+ - An `<svg>` with a `<title>` child names the button or link that contains it, so `button-name-present` no longer fails such a button.
89
+ - `progressbar-name-present` and `slider-name-present` accept an associated `<label>` on any labelable element, not only `<input type="range">`.
90
+ - The combobox, textbox, searchbox, spinbutton and slider name rules accept the placeholder of a text-like input or textarea as its name, as the accessible name computation does.
91
+ - `role-img-text-alternative-present` no longer reports an unnamed `<svg role="img">`, which `svg-text-alternative-present` already reports.
92
+ - English words left inside translated text are translated, and the terms each locale uses are consistent. Spanish said "landmark" in 39 strings while also using *región de referencia*; it now uses *región de referencia* throughout. German said "Header" for a table header cell (now *Kopfzelle*) and "Blending" (now *Mischmodi*); Spanish said "El caption de la tabla" (now `<caption>`). The role name `complementary` stays as code in German, Spanish and French, like `banner`, `contentinfo` and `main` already did. The Spanish description of `media-alternative-transcript-evidence` had translated the result code `cantTell`, which now stays as it is. `links-target-blank-noopener` read as a requirement in French and Spanish ("doivent"/"deben") although it only raises findings for review, and its French description said the rule fails a page; both now match the English.
93
+ - HTML report cards cap a long selector or summary at 220 characters, as the report's code always said they did: the helper that does it was written but never called, so a deeply nested selector could stretch a card across the page. The findings table and the embedded data keep the full values, and the hint is never shortened, since the table does not repeat it.
94
+ - The HTML report is written in the scan's language. Its headings, table columns, outcome and severity names, headline, notes and pager were hard-coded English, so a Japanese scan produced Japanese findings inside an English page. They now come from new `report_*` keys in every dictionary (all five locales), `<html lang>` names the locale, and dates and numbers use its format. The card heading now labels the severity ("(Fail, serious severity)") rather than listing two bare words, which also reads correctly in every language. A report for a locale the engine does not ship, from a caller-supplied dictionary, keeps English labels and marks its findings with their own `lang`. The report reads the dictionaries through two additions to the internal `__internal` export (`translate`, `resolveLocale`); nothing public changes.
95
+ - The rules that judge text by known phrases recognize German, Spanish, French and Japanese as well as English. `link-name-quality` (「こちら」, "Hier klicken", "Leer más", "En savoir plus"), `heading-quality` (「見出し」, 「第1章」, "Sans titre"), `form-control-label-quality` (「入力欄」, "Eingabefeld"), `page-title-patterns` (「トップページ」, "Startseite", "Inicio - …") and `media-alternative-transcript-evidence` (「文字起こし」, "Transkript", "transcripción") matched English only, so on a page in another language they missed what they exist to catch. English is always checked and the element's own language (nearest `lang`) is added, so a word that is generic in one language ("Suite", "Plus") is not flagged on an English page; transcript words are checked in every language. Text is NFKC-normalized, so full-width forms match, and trailing arrows no longer hide a generic link ("Read more »"). `page-title-patterns` counted characters, which made a complete Japanese title such as 「お問い合わせ」 "very short"; CJK characters now count double. The three rules' descriptions say which languages are covered, in every locale. English behavior is otherwise unchanged.
96
+ - The last findings whose text bypassed the dictionaries are translated. `css-hidden-focus` listed the hiding techniques it found as internal codes (`opacityZero,offscreen`) in every language; its summary now states each one as a sentence ("Its opacity is 0. It is positioned off-screen."), driven by one boolean parameter per technique, and `visibilityHints` stays in the parameters for callers that read it. The focus-redirect findings of `css-hidden-focus` and `iframe-focusable-content` were built in English with no dictionary key; they now use `cssHidden_focus_summary_cantTell_redirect`/`_hint_cantTell_redirect` and `iframeFocusableContent_summary_cantTell_redirect`/`_hint_cantTell_redirect`. All five locales. The English `css-hidden-focus` summary changes wording; finding fingerprints do not include message text, so baselines are unaffected.
97
+ - WCAG rollup titles and descriptions (`rulesResults[].title`/`.description`, the HTML report's "WCAG rollup" section, and composite entries from `getRulesCatalog()`) now follow the scan locale. The runner already localized a composite that named `meta.titleKey`/`meta.descriptionKey`, but none of the 34 did, and the nine `catalog.rules.*` entries in the dictionaries were never referenced, so every locale showed English rollups. Every composite now names its keys; 25 new title/description pairs are translated in all five locales, using each language's published success criterion names. English output is unchanged: `en.json` holds the catalog's exact text, and a test keeps the two in step.
98
+ - The three contrast rules now say what to do. `contrast-computable`'s `cantTell` findings, and the `fail` and engine-failure findings of `contrast-minimum` and `contrast-enhanced`, all had an empty hint, so a finding explained why contrast could not be measured, or that it was too low, and stopped there. `contrast-computable` now gives a hint per cause: a background image or gradient (`contrastComputable_hint_cantTell_background`), a blend mode, filter or text shadow (`_effect`), a transparent background up to the root (`_rootNotOpaque`); anything else, and an engine failure in any of the three rules, gets `contrast_hint_cantTell_manual`, which says to measure by hand and gives the ratios. The `fail` hints (`contrastMinimum_hint_fail`, `contrastEnhanced_hint_fail`) name the ratio the text has to reach. All five locales. Message text is not part of the finding fingerprint, so stored baselines are not affected.
99
+ - `aria-braille-equivalent` no longer puts English into translated messages. Both of its findings shared one template filled with `{{requires}}`, and for a missing accessible name that value was the English phrase "an accessible name", so every locale read "…aber nicht an accessible name…". Each case now has its own message: `ariaBrailleEquivalent_summary_fail_label`/`_hint_fail_label` and `ariaBrailleEquivalent_summary_fail_roleDescription`/`_hint_fail_roleDescription`, in all five locales, replacing `ariaBrailleEquivalent_summary_fail`/`_hint_fail`. The label hint also says how to add a name. `data.details.requires` is unchanged, and message text is not part of the finding fingerprint, so stored baselines are not affected. A caller dictionary (`engineOptions.messages`) that overrides the old keys falls back to English for this rule until it uses the new ones.
100
+ - The French text for `mediaTranscriptPresent_summary_cantTell_missing` dropped its `{{element}}` placeholder, so it never named the `<audio>` or `<video>` element concerned. Restored.
101
+ - The French hint for an unverified transcript (`mediaTranscriptPresent_hint_cantTell_unverified`) repeated the missing-transcript summary, with a literal `{element}` in it. It now says what to do, and the placeholder test also rejects single braces.
102
+ - SARIF and the HTML report no longer label EN 301 549 clauses as WCAG criteria. Both read every `normativeMappings` entry's `requirement` as a Success Criterion, so `manual-review`, which maps to EN 301 549 9.2.1.1, 9.2.4.3 and 9.2.4.7 alongside WCAG 2.1.1, 2.4.3 and 2.4.7, gained the SARIF tags `wcag-9.2.1.1`, `wcag-9.2.4.3` and `wcag-9.2.4.7`, and its report card showed a "WCAG 9.2.1.1" chip. Its Understanding-document entries also repeated each WCAG chip a second time. Only WCAG Success Criteria are tagged and chipped now; an entry that names no `standard` is still read as WCAG, as before. EARL already made this distinction and is unchanged. Tag and chip text only: finding fingerprints and stored baselines are not affected.
103
+ - The shared DOM-eligibility cache could leak an `opacity:0` verdict between unrelated callers. Its cache key covered `visibilityMode`/`disableGeometry` but not `ignoreOpacity`, so a caller that explicitly asks to ignore opacity-based hiding (`queryAllSmart`'s own inert check, matching `visibilityMode: 'styleOnly', disableGeometry: true`) shared a bucket with five callers that don't (`aria-hidden-focus`'s two eligibility checks, and the manual `area-alt-quality`, `css-focus-indicator-suppressed`, `css-hidden-focus`, `form-control-label-quality` rules, all using the identical opts minus `ignoreOpacity`). Whichever ran first for a given `opacity:0` element within a scan could leave its verdict cached for the other, incorrectly. `ignoreOpacity` is now part of the cache key, so each combination gets its own bucket. Verified directly: an isolated call correctly excludes an `opacity:0` element (`reasons: ['opacityZero']`), but previously returned eligible when preceded by an `ignoreOpacity: true` call on the same element; now it doesn't. No rule's fixture or test changed, confirming the collision wasn't already covered — a regression test was added directly against the cache (`tests/cache-tests/dom-helpers-eligibility-cache.test.js`) rather than relying on hitting it through a real scan.
104
+ - `deprecated-elements-not-used` and `server-side-image-map-absent` report `pass` on a clean page instead of `notApplicable`. Both query directly for the violation (`blink, marquee`; `img[ismap]`), so every match already became an occurrence and the two outcomes exhausted every case — the `pass` branch each carried was dead code. Both rules' own `@expectation` already phrases the clean case as an absence claim ("neither element is present" / "no image uses ismap"), which is simply true when nothing is found, not inapplicable — matching how `aria-hidden-body`, `page-title-present`, and the rest of this shape already behave. The practical effect lands hardest on `deprecated-elements-not-used`: it is the sole atomic contributor to the `wcag-2.2.2-pause-stop-hide` composite, so that composite could never report `pass` for any page, ever — a fully clean scan showed SC 2.2.2 as "not applicable" rather than "passes." `server-side-image-map-absent` shares 4 sibling contributors in `wcag-2.1.1-keyboard`, so the same limitation there was already masked almost all the time. Neither rule emits an occurrence on the clean path, so no finding fingerprint or stored baseline is affected — this only changes the rule- and composite-level `outcome` field.
105
+ - `aria-braille-equivalent`'s `cantTell` message read "has aria-braillelabel but no an accessible name" — a double negative from a shared template ("but no {{requires}}") reused for a value that already carried its own article ("an accessible name"). Changed the template to "but not {{requires}}", which reads correctly for both call sites (the other fills `{{requires}}` with the bare attribute name `aria-roledescription`, which "not" also suits). English only; `de`/`fr` already used the "not" phrasing and `es`'s "no" already serves that role in Spanish. Message text only, not part of the finding fingerprint, so no stored baseline is affected.
106
+ - `inert` on an `<area>` itself, or on its `<map>`, now excludes the area the same as `inert` anywhere else, instead of being silently ignored. The exception dated to the project's first commit and had no recorded rationale; it read as a generalization of the correct, separate rule that `aria-hidden` on a focusable element does not remove it from eligibility, since a real user can still Tab to it. `inert` is not `aria-hidden`: the HTML spec has it remove focusability outright, with no image-map carve-out, and `aria-hidden-focus.js`'s own inert check (`hasInertAncestor`) already treated `<area>`/`<map>` with no special case, so the two rules disagreed on the same input. `hasBlockingInert` in `dom-helpers.js` drops the carve-out; an inert `<map>` or `<area>` is `notApplicable` now, matching how an inert ancestor outside the map already behaved.
107
+ - `avoid-inline-spacing` and `link-in-text-block` no longer discard their `cantTell` findings when something else on the page fails. Both built an undecided list and then returned early with the fail list alone, so a single spacing failure silently removed every element the rule could not decide — the one thing an engine that reports what it cannot tell you must not do. `helpers.resolveTieredOutcome` exists to merge the two tiers and `target-size-minimum` already used it; both rules now do the same. The rule-level outcome is unchanged, `fail` where it was `fail`, and the confident failures are byte-identical; what changes is that the result carries the `cantTell`-tier occurrences alongside them, each with `occurrenceOutcome: "cantTell"` and the `uncertainty` block naming what a human would have to settle. On their own fixtures that is three more occurrences for `avoid-inline-spacing` — a `calc()` that resolves to no ratio, and two elements whose text cannot take a soft wrap break — and one more for `link-in-text-block`, a link over a background image whose contrast against the surrounding text is not computable. `link-in-text-block` gains the `BACKGROUND_IMAGE_OR_GRADIENT` reason code in `scripts/data/finding-ids.json`, which the rule always had but could never emit on a page that also failed; the addition is purely additive, so no existing finding fingerprint or stored baseline changes.
108
+
7
109
  ## [1.7.0] - 2026-08-29
8
110
 
9
111
  ### Added
@@ -37,7 +139,7 @@ All notable changes to this project are documented here, in [Keep a Changelog](h
37
139
  - `docs/RULE_TAXONOMY.md` §1.1 no longer says an automatic rule may use `cantTell` only as a defensive fallback, "never as its primary intended path". Six rules now lead with it. The dividing line between `automatic` and `manual` is whether a rule can decide, not which outcome it reports: an automatic rule reporting `cantTell` has decided and is saying what it found, while a manual rule reports `cantTell` because the question is not decidable from markup. The three cases where `cantTell` is an automatic rule's primary path are listed there.
38
140
 
39
141
  ### Fixed
40
- - A frame responder answered any window that could reach it, not just the frame embedding it. `a11yCoreEnableFrameResponder()` is documented as opting a frame in to being scanned *from above*, but the listener ran a scan for any sender: a sibling frame can obtain a reference through `parent.frames[i]` and `postMessage` across origins, and the reply carries `occurrences[].html` — DOM content the same-origin policy gives that sibling no way to read. An opener could do the same. A `run` command is now answered only when its sender is the direct parent, which the hop-by-hop relay makes the only legitimate case, and a window nothing embeds answers nobody. **Affects 1.3.0 through 1.6.0**, and only consumers that call `a11yCoreEnableFrameResponder()` in a framed page — the automation-driver patterns (`@surea11y/playwright`, `@surea11y/puppeteer`, the CLI) never use the responder and were never exposed.
142
+ - A frame responder answered any window that could reach it, not just the frame embedding it. `a11yCoreEnableFrameResponder()` is documented as opting a frame in to being scanned *from above*, but the listener ran a scan for any sender: a sibling frame can obtain a reference through `parent.frames[i]` and `postMessage` across origins, and the reply carries `occurrences[].html` — DOM content the same-origin policy gives that sibling no way to read. An opener could do the same. A `run` command is now answered only when its sender is the direct parent, which the hop-by-hop relay makes the only legitimate case, and a window nothing embeds answers nobody. **Affects every published version before 1.7.0**, and only consumers that call `a11yCoreEnableFrameResponder()` in a framed page — the automation-driver patterns (`@surea11y/playwright`, `@surea11y/puppeteer`, the CLI) never use the responder and were never exposed.
41
143
  - A reply could be accepted from a window the request never went to. Any window naming an in-flight `requestId` could settle it, forging a frame's scan result or its failure. Each pending request now records the window it was sent to and ignores answers from anywhere else, pings included — reachability is the addressed frame's verdict to give.
42
144
  - `engineOptions.excludeSelectors` cost a multiple of the whole scan. Every rule queries through `isExcluded`, which walked an element's ancestor chain once per selector with nothing remembered between rules, so the work was repeated for all 130 of them: excluding a cookie banner and four ad slots turned a 2.3s scan of a 3574-element page into 10.5s, and 25 exclusions — an ordinary list for a real site — into 43s. Exclusion results are now memoized per element for the run, partitioned by the effective exclude list exactly as the selector cache already is, and the self-test uses `matches` against the element rather than `closest` against the chain, since a parent's answer already settles its descendants. The same page with 25 exclusions takes 3.1s, and the raw matching floor is 18ms against the 5588ms an equivalent scan used to spend. Rule-scoped `rules[ruleId].excludeSelectors` keep their own cache partition, and a malformed selector still excludes nothing without disabling the usable selectors beside it. `perfStats` gains `excluded.hit`/`excluded.miss`.
43
145
  - A rule that could not check anything said so, and SARIF dropped it. The contrast rules attach an occurrence to their `notApplicable` result naming the eligible text count and pointing at `contrast-computable`; the HTML report showed it, SARIF did not, so a CI pipeline reading only SARIF saw no contrast alerts and had no way to tell a clean page from one where contrast was never computable. Those occurrences now reach SARIF as `note`-level entries in `runs[0].invocations[0].toolExecutionNotices`, each carrying `associatedRule.id`. They are deliberately not results: every SARIF result renders as an alert, and "not evaluated" is not one, so nothing new gates a build or appears in Code Scanning. The block is emitted only when there is something to say.
package/README.md CHANGED
@@ -6,30 +6,79 @@
6
6
  [![node](https://img.shields.io/node/v/@surea11y/core?style=flat-square&label=node&labelColor=101413&color=3A4441)](package.json)
7
7
  [![license](https://img.shields.io/badge/license-MPL--2.0-3A4441?style=flat-square&labelColor=101413)](LICENSE)
8
8
 
9
+ [Website](https://surea11y.dev/) · [Documentation](https://surea11y.dev/getting-started/) · [Rules](https://surea11y.dev/rules/)
10
+
9
11
  > **Accessibility testing that tells you what it can't tell you.**
10
12
 
11
- surea11y is an accessibility engine for teams that need to know what automated
12
- testing *can't* establish. It reports findings, non-findings, and — unusually —
13
- explicit uncertainty, so results are auditable rather than reassuring.
13
+ `@surea11y/core` is the WCAG accessibility testing engine behind the surea11y
14
+ family of packages. It runs in Node.js against a DOM you supply (such as
15
+ jsdom) or inside a real browser page, and returns deterministic,
16
+ standards-traceable results. The integrations for Playwright, Cypress,
17
+ Puppeteer, Selenium, WebdriverIO, Jest/Vitest and the command line are
18
+ separate packages built on this engine: see
19
+ [Which package do I need?](#which-package-do-i-need).
20
+
21
+ What sets it apart is what it does with the cases automated testing can't
22
+ settle. It implements the W3C's open [Accessibility Conformance Testing (ACT)
23
+ Rules Format](https://www.w3.org/TR/act-rules-format/), verified against
24
+ ACT's own published test corpus rather than judged only against itself, and
25
+ it reports findings, non-findings, and (unusually) explicit uncertainty, so
26
+ results are auditable rather than reassuring.
14
27
 
15
28
  *Sure* means certainty about what is known, and honesty about what isn't.
16
29
 
17
- It runs against either static HTML or fully rendered browser pages, producing
18
- deterministic, standards-traceable results suitable for local development,
19
- automated testing and CI/CD pipelines.
20
-
21
- Unlike browser extensions or cloud-based services, surea11y is a library-first
22
- project. You install it, run it where your code runs, and receive structured
23
- results that can be consumed by people, scripts or reporting tools.
30
+ 132 accessibility rules · 58 validated against the ACT corpus (798 reference
31
+ cases) · zero runtime dependencies
32
+
33
+ Unlike browser extensions or cloud-based services, the engine is a library.
34
+ You install it, run it where your code runs, and receive structured results
35
+ that can be consumed by people, scripts or reporting tools.
36
+
37
+ ## Contents
38
+
39
+ - [Goals](#goals)
40
+ - [What automated testing can and cannot do](#what-automated-testing-can-and-cannot-do)
41
+ - [What this engine does not detect](#what-this-engine-does-not-detect)
42
+ - [Key principles](#key-principles)
43
+ - [Choosing the right execution model](#choosing-the-right-execution-model)
44
+ - [Which package do I need?](#which-package-do-i-need)
45
+ - [Installation](#installation)
46
+ - [Quick Start](#quick-start)
47
+ - [Understanding the Results](#understanding-the-results)
48
+ - [Documentation](#documentation)
49
+ - [Philosophy](#philosophy)
50
+ - [Project Structure](#project-structure)
51
+ - [Building the Project](#building-the-project)
52
+ - [Contributing](#contributing)
53
+ - [Security](#security)
54
+ - [Versioning & stability](#versioning--stability)
55
+ - [Maintainer](#maintainer)
56
+ - [License](#license)
57
+
58
+ ## Goals
59
+
60
+ 1. **Say only what can be established, and say it plainly when it can't.**
61
+ `fail` is reserved for objective, normative violations; `cantTell` exists
62
+ so an ambiguous case is reported as ambiguous rather than silently
63
+ dropped or guessed at. A shorter report is not a goal in itself.
64
+
65
+ 2. **Verify against an open standard, not just internal tests.** Every rule
66
+ with a W3C ACT Rules counterpart runs against ACT's own published test
67
+ corpus — 798 examples across 58 rules — and the results are public. See
68
+ [Checked against the ACT corpus](#checked-against-the-act-corpus) below.
69
+
70
+ 3. **Make the engine and its rules approachable to build on.** Custom rules,
71
+ policies, and framework bindings are first-class extension points, and
72
+ every rule ships with its WCAG mapping, applicability, and expectation
73
+ documented. Worked examples are available in
74
+ [`docs/RULE_EXAMPLES.md`](./docs/RULE_EXAMPLES.md), and the complete rule
75
+ catalog can be browsed at [surea11y.dev/rules](https://surea11y.dev/rules/).
24
76
 
25
77
  ## What automated testing can and cannot do
26
78
 
27
- Automated tools are commonly reckoned to catch somewhere around a third of WCAG
28
- issues. The remainder require human judgement. That ceiling is a property of
29
- static analysis itself, not a gap in any particular tool.
30
-
31
- surea11y's answer is to be explicit about which side of that line every result
32
- falls on. Each rule makes a single deterministic decision:
79
+ Many WCAG requirements cannot be determined through automated testing alone and
80
+ require human judgement. surea11y's answer is to make that boundary explicit.
81
+ Each rule makes a single deterministic decision:
33
82
 
34
83
  - **`fail`** — a violation provable from the DOM. Reserved for objective,
35
84
  normative cases.
@@ -56,18 +105,38 @@ such as whether a heading describes the content under it.
56
105
  reasoning, and `node scripts/act-testcase-check.js` reproduces the figures. They
57
106
  cover the rules that have an ACT counterpart.
58
107
 
108
+ The [EARL implementation report](https://surea11y.github.io/act-report/act-report.jsonld)
109
+ records the outcome for every one of those cases, passes and inapplicable
110
+ results included, so a rule that stayed silent because nothing applied is
111
+ distinguishable from one that is not implemented. `node scripts/act-report.js`
112
+ regenerates it against the corpus as it stands rather than a snapshot, and a
113
+ scheduled job republishes it weekly and on release.
114
+
59
115
  ## What this engine does not detect
60
116
 
61
- Keyboard traps, reflow and clipping at 400% zoom, anything that only exists
62
- after a click or an async load, and judgement calls such as whether a heading is
63
- meaningful — these lie outside what a static DOM scan can establish. Each is a
64
- reasoned decision rather than an oversight.
117
+ Some things lie outside what a scan of the DOM can establish. For example,
118
+ the engine will not:
65
119
 
66
- [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md) lists them in full with the
67
- reasoning for each. A `pass` from this engine — or from any automated tool — is
68
- never a substitute for the manual review WCAG itself requires.
120
+ - confirm that alt text is *meaningful*, only that it is present
121
+ (an alt attribute of `"image123.png"` passes the objective check);
122
+ - judge whether a color contrast choice is aesthetically appropriate,
123
+ only whether it meets the applicable contrast ratio;
124
+ - determine whether an error message actually *explains* the problem,
125
+ since that depends on validation logic a static scan can't see;
126
+ - detect a keyboard trap, or reflow and clipping at 400% zoom, since both
127
+ require simulating real user interaction over time;
128
+ - see anything that only exists after a click or an async load.
69
129
 
70
- ### Key principles
130
+ Where a case comes down to judgement, such as whether a heading is
131
+ meaningful, the engine reports `cantTell` rather than guessing. Each of these
132
+ boundaries is a reasoned decision rather than an oversight:
133
+ [Known limitations](https://surea11y.dev/help/known-limitations/) lists them
134
+ in full with the reasoning for each (also in
135
+ [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md)). A `pass` from this engine, or
136
+ from any automated tool, is never a substitute for the manual review WCAG
137
+ itself requires.
138
+
139
+ ## Key principles
71
140
 
72
141
  - **Deterministic execution.** The same input always produces the same
73
142
  output.
@@ -83,21 +152,22 @@ never a substitute for the manual review WCAG itself requires.
83
152
  by rule IDs, tags or WCAG version.
84
153
  - **Localized reporting.** Human-readable messages can be translated
85
154
  without affecting machine-readable data. Ships with `en`, `fr`, `de`,
86
- and `es` today — see [`docs/I18N.md`](./docs/I18N.md) to use one or
155
+ `es` and `ja` today. See [`docs/I18N.md`](./docs/I18N.md) to use one or
87
156
  contribute another.
88
157
 
89
158
  ---
90
159
 
91
160
  ## Choosing the right execution model
92
161
 
93
- surea11y supports two complementary execution models. Choosing the
162
+ The engine supports two complementary execution models. Choosing the
94
163
  correct one is essential because it determines which parts of the page
95
164
  the engine can inspect.
96
165
 
97
166
  ### Static HTML
98
167
 
99
- The CLI (`scan file.html` or `scan https://example.com`) and
100
- `runDomRulesInPage()` analyse HTML without executing page JavaScript.
168
+ `runDomRulesInPage()` analyses HTML without executing page JavaScript,
169
+ and so does the separate [`@surea11y/cli`](#cli) package, which wraps it
170
+ for the terminal.
101
171
 
102
172
  This approach is ideal for:
103
173
 
@@ -148,7 +218,9 @@ matches how you test — each pulls in `@surea11y/core` for you.
148
218
  | Scan static HTML from a **terminal or CI pipeline** | [`@surea11y/cli`](https://github.com/SureA11y/cli#readme) |
149
219
  | Run the engine against **a DOM I already have** | `@surea11y/core` (this package) |
150
220
 
151
- The rest of this README covers `@surea11y/core` itself.
221
+ [Getting Started](https://surea11y.dev/getting-started/) on surea11y.dev
222
+ covers installation and usage for each of these. The rest of this README
223
+ covers `@surea11y/core` itself.
152
224
 
153
225
  ---
154
226
 
@@ -343,6 +415,33 @@ embedded frame to also load the engine and opt in, which doesn't fit a
343
415
  single dropped-in `<script>` tag; reach for the npm package directly if
344
416
  you need that.
345
417
 
418
+ ### Conformance targets and EN 301 549
419
+
420
+ To test against a named target instead of a hand-picked tag list, pass a
421
+ profile:
422
+
423
+ ```js
424
+ runDomRulesInPage(url, null, { profile: 'en301549-v3.2.1' }, null);
425
+ ```
426
+
427
+ `wcag22-aa`, `en301549-v4.1.1`, `en301549-v3.2.1` and `section508` each run
428
+ the WCAG Level A and AA rules of the version they build on; the result records
429
+ the one used in `engine.profile`. A profile only chooses which rules
430
+ run; it does not certify conformance. A standard with verdicts of its own
431
+ comes as a profile under [`profiles/`](./profiles/README.md), with its own
432
+ rules, which run only when a scan targets it; to run every rule instead, pass
433
+ `optInRules: 'all'` with no profile; the result records it in
434
+ `engine.optInRules`.
435
+
436
+ An EN 301 549 profile also maps every WCAG criterion in the result to the
437
+ clause of that version that restates it (1.4.3 to 9.1.4.3, for example), and
438
+ the SARIF, JUnit and HTML reports carry those clauses. To get the clauses
439
+ without the profile, or for both versions, pass
440
+ `mappings: ['en301549']` (or `'en301549:V3.2.1'`); by default a result names
441
+ WCAG only. See
442
+ [`docs/ENGINE_OPTIONS.md`](./docs/ENGINE_OPTIONS.md#conformance-profiles) and
443
+ [`docs/WCAG_CONFORMANCE.md`](./docs/WCAG_CONFORMANCE.md#en-301-549).
444
+
346
445
  ---
347
446
 
348
447
  ## Understanding the Results
@@ -406,14 +505,25 @@ The complete schema also includes confidence, severity, WCAG
406
505
  traceability, composite rule results and other metadata intended for
407
506
  reporting and automation.
408
507
 
409
- For a complete field-by-field reference, see `docs/OUTPUT_SCHEMA.md`.
508
+ For a complete field-by-field reference, see
509
+ [Output schema](https://surea11y.dev/results-reports/output-schema/)
510
+ (`docs/OUTPUT_SCHEMA.md` in the repository).
410
511
 
411
512
  ---
412
513
 
413
514
  ## Documentation
414
515
 
415
- The project documentation is organized by topic so you can start quickly
416
- and progressively explore more advanced features.
516
+ Full documentation is available at **[surea11y.dev](https://surea11y.dev/)**:
517
+ [getting started](https://surea11y.dev/getting-started/) for each integration,
518
+ the [rule catalog](https://surea11y.dev/rules/),
519
+ [configuration](https://surea11y.dev/configuration/),
520
+ [results and reports](https://surea11y.dev/results-reports/),
521
+ [WCAG conformance](https://surea11y.dev/conformance/), and
522
+ [help](https://surea11y.dev/help/).
523
+
524
+ The repository also contains the following technical and contributor
525
+ documentation, useful as direct references when building on the engine or
526
+ contributing to it:
417
527
 
418
528
  | Document | Description |
419
529
  |---|---|
@@ -422,12 +532,13 @@ and progressively explore more advanced features.
422
532
  | `docs/BASELINE.md` | CI baseline/allowlist: gate builds only on new violations. |
423
533
  | `docs/REPORT.md` | Self-contained HTML report: browsable summary, WCAG rollup, filterable occurrence table. |
424
534
  | `docs/SARIF.md` | SARIF 2.1.0 report for GitHub Code Scanning and other SARIF dashboards. |
535
+ | `docs/JUNIT.md` | JUnit XML report for the test dashboards of GitLab, Azure DevOps, Jenkins and CircleCI. |
425
536
  | `docs/EARL.md` | EARL 1.0 report in JSON-LD: the W3C interchange format, and the ACT implementation-report format. |
426
537
  | `docs/CI_INTEGRATIONS.md` | GitHub Actions and Bitbucket Pipelines templates wrapping the CLI. |
427
538
  | `docs/ENGINE_OPTIONS.md` | Configuration, filtering, policies and localization. |
428
539
  | `docs/INTEGRATION.md` | Using surea11y with jsdom, Playwright, Puppeteer, Selenium, Cypress and other drivers. |
429
540
  | `docs/BINDING_AUTHORS_GUIDE.md` | Building new framework integrations on top of the engine. |
430
- | `docs/RULE_CATALOG.md` | Reference of every built-in accessibility rule. |
541
+ | `docs/RULE_CATALOG.md` | Reference of every built-in accessibility rule; a profile's own rules are in its catalog, in `profiles/<key>/docs/RULE_CATALOG.md`. |
431
542
  | `docs/WCAG_CONFORMANCE.md` | Understanding WCAG rollups and conformance reporting. |
432
543
  | `docs/POLICY.md` | Built-in policy contracts and customization. |
433
544
  | `docs/I18N.md` | Translation support and localization. |
@@ -459,27 +570,11 @@ need human judgement, knowledge of context, or usability evaluation. A
459
570
  single score or a pass/fail verdict flattens that difference; surea11y
460
571
  reports it.
461
572
 
462
- This is why `cantTell` and `notApplicable` exist as outcomes, and it
463
- shapes every rule in the engine.
464
-
465
- ### What surea11y won't catch
466
-
467
- Being explicit about the boundaries of automation is part of the same
468
- philosophy. For example, surea11y will not:
469
-
470
- - confirm that alt text is *meaningful*, only that it is present
471
- (an alt attribute of `"image123.png"` passes the objective check);
472
- - judge whether a color contrast choice is aesthetically appropriate,
473
- only whether it meets the applicable contrast ratio;
474
- - determine whether an error message actually *explains* the problem,
475
- since that depends on validation logic a static scan can't see;
476
- - detect a keyboard focus trap or content clipped at 400% zoom, since
477
- both require simulating real user interaction over time, not just
478
- reading the DOM at one instant.
479
-
480
- These are the cases where the engine reports `cantTell`, and where a
481
- human reviewer's judgement remains necessary. See
482
- `docs/LIMITATIONS.md` for the complete list of structural limitations.
573
+ This is why `cantTell` and `notApplicable` exist as outcomes (see
574
+ [What automated testing can and cannot do](#what-automated-testing-can-and-cannot-do)),
575
+ and why the engine is explicit about
576
+ [what it does not detect](#what-this-engine-does-not-detect). It shapes
577
+ every rule in the engine.
483
578
 
484
579
  ---
485
580
 
@@ -498,7 +593,11 @@ src/
498
593
  baseline.js # Baseline entry point (@surea11y/core/baseline)
499
594
  report.js # HTML report entry point (@surea11y/core/report)
500
595
  sarif.js # SARIF entry point (@surea11y/core/sarif)
596
+ junit.js # JUnit XML entry point (@surea11y/core/junit)
501
597
  earl.js # EARL entry point (@surea11y/core/earl)
598
+ en301549.js # EN 301 549 clause table (@surea11y/core/en301549)
599
+ wcag.js # WCAG criteria per version (@surea11y/core/wcag)
600
+ profile-kit.js # Mapping for a profile made with profile:new (internal, not exported)
502
601
 
503
602
  checks/
504
603
  automatic/ # Deterministic automated rules
@@ -511,6 +610,10 @@ src/
511
610
  catalogs/ # Composite rule catalogs
512
611
  explain/ # Occurrence grouping, internal
513
612
 
613
+ profiles/
614
+ index.js # The profiles built into the engine (none yet)
615
+ README.md # What a profile holds and how to add one
616
+
514
617
  scripts/
515
618
  build-core.js # Generates src/core.js
516
619
  build-browser.js # Generates the browser bundle and its locale side files
@@ -3,11 +3,11 @@
3
3
  Cross-reference between the [W3C ACT Rules](https://act-rules.github.io/rules/) (as published at act-rules.github.io) and this repo's rule catalog (`docs/RULE_CATALOG.md`). Built by matching rule names/descriptions and WCAG SC, not machine-generated, so treat close calls as a starting point for review rather than ground truth.
4
4
 
5
5
  **Summary (117 active ACT rules, excludes 3 deprecated):**
6
- - **58 confirmed direct, family, or partial matches** in our automatic/manual rules (`scripts/data/act-rule-map.json` is the machine-readable version of the table below)
6
+ - **59 confirmed direct, family, or partial matches** in our automatic/manual rules (`scripts/data/act-rule-map.json` is the machine-readable version of the table below)
7
7
  - **~2** are covered structurally by our composite/rollup layer, not a named rule
8
8
  - **~46 are gaps**, no corresponding rule in this repo, listed in [Gaps](#gaps-no-corresponding-rule) below
9
9
 
10
- **Every matched rule has now been run through ACT's own official test-case corpus** (`scripts/act-testcase-check.js`, 713 test cases across the 51-rule matched set of the time). Started at 86 mismatches; real bugs were fixed, mapping errors corrected, and every remaining mismatch triaged into a scope difference, a jsdom/environment limit, or a genuine open design question (tracked in [`docs/DESIGN_CHALLENGES.md`](./DESIGN_CHALLENGES.md)). A second pass then re-ran the whole corpus from a local checkout (see "Second pass" below) and repeated the exercise on what it turned up. Current state: **798 examples across the 58-rule matched set, 31 mismatches, all explained** below or in that file, and — the figure that matters for an implementation report — **zero false positives**: no example ACT declares `passed` or `inapplicable` is failed by this engine, so every remaining mismatch is a case it does not catch rather than one it gets wrong; see "Progress" further down for the full per-rule breakdown. (The exact example count drifts slightly over time as ACT's own published corpus gains or loses cases; re-run `scripts/act-testcase-check.js` for the live figure rather than trusting this number indefinitely.)
10
+ **Every matched rule has now been run through ACT's own official test-case corpus** (`scripts/act-testcase-check.js`, 713 test cases across the 51-rule matched set of the time). Started at 86 mismatches; real bugs were fixed, mapping errors corrected, and every remaining mismatch triaged into a scope difference, a jsdom/environment limit, or a genuine open design question (tracked in [`docs/DESIGN_CHALLENGES.md`](./DESIGN_CHALLENGES.md)). A second pass then re-ran the whole corpus from a local checkout (see "Second pass" below) and repeated the exercise on what it turned up. Current state: **821 examples across the 59-rule matched set, 30 mismatches, all explained** below or in that file, and — the figure that matters for an implementation report — **zero false positives**: no example ACT declares `passed` or `inapplicable` is failed by this engine, so every remaining mismatch is a case it does not catch rather than one it gets wrong; see "Progress" further down for the full per-rule breakdown. (The exact example count drifts slightly over time as ACT's own published corpus gains or loses cases; re-run `scripts/act-testcase-check.js` for the live figure rather than trusting this number indefinitely.)
11
11
 
12
12
  Real rule bugs found and fixed this way, in rough chronological order:
13
13
  - `button-name-present` wasn't crediting the UA-default label on `input[type=submit]`/`input[type=reset]` with no `value`, and wasn't honoring `role="none"`/`role="presentation"` conflict-resolution.
@@ -74,11 +74,11 @@ We also have automatic rules with **no ACT counterpart at all** (see [Extra cove
74
74
 
75
75
  ### Progress: full validation results, by ACT rule
76
76
 
77
- **Clean (0 mismatches):** `5f99a7`, `80f0bf`, `4c31df`, `73f2c2`, `97a4e1`, `cf77f2`, `b40fd1`, `46ca7f`, `6cfa84`, `307n5z`, `4e8ab6`, `a25f45`, `ffd0e9`, `b5c3f8`, `2779a5`, `5b7ae0`, `bf051a`, `qt1vmo`, `59796f`, `23a2a8`, `24afc2`, `9e45ec`, `c487ae`, `m6b1q3`, `bc659a`, `bisz58`, `b4f0c3`, `674b10`, `0ssw9k`, `3ea0c8`, `5c01ea`, `bc4a75`, `2ee8b8`, `e88epe`, `7d6734`, `de46e4`, `6a7281`, `8fc3b6`, `akn7bn`, `fd3a94`, `b20e66`, `cae760`, `78fd32` (43 of 58 matched rules).
77
+ **Clean (0 mismatches):** `5f99a7`, `80f0bf`, `4c31df`, `73f2c2`, `97a4e1`, `cf77f2`, `b40fd1`, `46ca7f`, `6cfa84`, `307n5z`, `4e8ab6`, `a25f45`, `ffd0e9`, `b5c3f8`, `2779a5`, `5b7ae0`, `bf051a`, `qt1vmo`, `59796f`, `23a2a8`, `24afc2`, `9e45ec`, `c487ae`, `m6b1q3`, `bc659a`, `bisz58`, `b4f0c3`, `674b10`, `0ssw9k`, `3ea0c8`, `5c01ea`, `bc4a75`, `2ee8b8`, `e88epe`, `7d6734`, `de46e4`, `6a7281`, `8fc3b6`, `akn7bn`, `fd3a94`, `b20e66`, `cae760`, `78fd32`, `b33eff`, `e086e5` (45 of 59 matched rules).
78
78
 
79
79
  Two rules changed what they report after this table was last regenerated, both from `fail` to `cantTell`: `3ea0c8`/`duplicate-id` under the default WCAG 2.2 target, and `6a7281`/`aria-valid-attr-value` for an unresolved `aria-controls` target (see `docs/DESIGN_CHALLENGES.md`). The verdicts above should still hold, since `evaluate()` in `scripts/act-testcase-check.js` counts a `cantTell` carrying occurrences as satisfying an ACT "failed" expectation and a `cantTell` never breaks a "passed" one, but neither was re-run against the live corpus at the time (the site was unreachable from that environment). Re-run both when convenient.
80
80
 
81
- **Remaining mismatches (31 total), all triaged.** Every one is an ACT `failed` example this engine does not flag — a coverage gap, which a partially consistent implementation is allowed — not an example it fails wrongly:
81
+ **Remaining mismatches (30 total), all triaged.** Every one is an ACT `failed` example this engine does not flag — a coverage gap, which a partially consistent implementation is allowed — not an example it fails wrongly:
82
82
 
83
83
  | ACT ID | Mismatches | Category |
84
84
  |---|---|---|
@@ -86,10 +86,10 @@ Two rules changed what they report after this table was last regenerated, both f
86
86
  | `aaa1bf` | 1 | different question, not a gap in ours: `aaa1bf`'s own applicability/expectation is purely about clip *duration* ("does the audio stay under 3s," explicitly not exempted by a `controls` mechanism); `no-autoplay-audio` answers WCAG 1.4.2's other disjunct instead (does a pause/stop/volume mechanism exist). Duration isn't knowable from static markup regardless, no browser decodes media at scan time, so this mismatch can't close even in principle, not because our rule falls short of it |
87
87
  | `ye5d6e` | 1 | scoped leniency: whether repeated-boilerplate content wraps the skip target is a cross-page judgment undecidable from one document; the rule's own header comment already reasons through this trade-off |
88
88
  | `047fe0` | 2 | one of each: scoped leniency (a heading sitting inside the repeated `<nav>` block itself, same cross-page judgment as `ye5d6e` above), and an env/harness limit (a heading positioned off-screen via a `<link>`ed external stylesheet the test fetcher doesn't load, same class as `oj04fd` below; `tests/engine-checks/manual/bypass-blocks-present.test.js` pins the real, fixed behavior with the same CSS inlined) |
89
- | `e086e5` | 2 | accepted divergence, decided 2026-08-19, see `docs/DESIGN_CHALLENGES.md`'s "Decided" section: `<label for>`/wrapping association stays honoured on non-natively-labelable ARIA widgets, because a screen reader that announces such a label makes "no accessible name" a false positive. Real AT behaviour wins over the spec reading here |
90
89
  | `oj04fd` | 1 | env/harness limit: ACT's one failed example keeps its `outline: none` in a linked stylesheet, which the example runner does not fetch, so no focus rule is visible to parse at all. Inlining that same CSS reports the element (`tests/engine-checks/manual/css-focus-indicator-suppressed.test.js` pins it); a real page hands the engine its stylesheets through the CSSOM |
91
90
  | `cc0f0a` | 3 | inherent limitation: `form-control-label-quality` catches the three deterministic shapes (a placeholder label, a label repeated with no visible heading/legend/row telling the fields apart, a label split between visible and hidden parts, which covers ACT's failed examples 4 and 5). The remaining three fail on the meaning of a well-formed word, `<label>Menu</label>` over a first-name field, which no markup-level check reaches |
92
91
  | `b49b2e` | 5 | inherent limitation: `heading-quality` catches placeholder heading text (a generic word, a numbered template slot, a filename, a URL), which is the deterministic half of this rule; whether a well-formed heading actually describes the content after it is a reading-comprehension judgment, and all 5 of ACT's failed examples are exactly that shape ("Weather" over opening hours, across five different heading-naming mechanisms) |
92
+ | `4b1c6c` | 1 | env/harness limit: Failed Example 4 nests the second frame inside another frame's `srcdoc`, which jsdom does not load, so only one frame carries the shared name. `identical-iframes-same-purpose` never fails by design: frames embedding different resources may still be equivalent, so it reports `cantTell`, which counts as flagged |
93
93
  | `aizyf1` | 2 | inherent tension with `5effbb`'s own examples, not a bug: both remaining cases (`<a>this product</a>` after "See the description of", and a format-name list under an "Ulysses" heading) are ACT's own *passed* examples for `5effbb` (context-aware) but *failed* examples for `aizyf1` (context-blind: the accessible name alone, ignoring what makes it clear, must already be descriptive). One shared rule can credit context or not, not both on the same markup; `link-name-quality` sides with `5effbb`'s reading, which is what its context-detection is for |
94
94
  | `5effbb` | 1 | genuine judgment gap: the one remaining case (`<a>Workshop</a>` after an unrelated paragraph) needs to judge whether an ordinary word actually relates to nearby prose, not a phrase-list or context-structure question `link-name-quality` can answer |
95
95
  | `d0f69e` | 3 | documented false-negative policy: `table-th-has-data-cells`'s own header comment explains it only catches the unambiguous "zero data cells anywhere" case, not real positional header-association (the new ARIA-grid coverage added during this pass is real but doesn't happen to close these 3 specific positional-mismatch cases) |
@@ -130,6 +130,7 @@ Two rules changed what they report after this table was last regenerated, both f
130
130
  | `5b7ae0` | HTML page lang/xml:lang attributes match | `html-xml-lang-mismatch` | exact |
131
131
  | `bf051a` | HTML page lang attribute has valid language tag | `html-lang-attr-present` | exact |
132
132
  | `3ea0c8` | Id attribute value is unique | `duplicate-id` | exact |
133
+ | `4b1c6c` | Iframe elements with identical accessible names have equivalent purpose | `identical-iframes-same-purpose` | partial |
133
134
  | `cae760` | Iframe element has non-empty accessible name | `iframe-name-present` | exact |
134
135
  | `akn7bn` | Iframe with negative tabindex has no interactive content | `iframe-focusable-content` | exact |
135
136
  | `qt1vmo` | Image accessible name is descriptive | `img-alt-quality`, `canvas-text-alternative-quality`, `svg-text-alternative-quality` (all manual) | family |
@@ -187,7 +188,7 @@ Grouped by theme, with WCAG SC where ACT declares one:
187
188
  - `3e12e1` Block of repeated content is collapsible
188
189
 
189
190
  **Judgment-call gaps found during test-case validation:**
190
- - `4b1c6c` "Iframes with identical accessible names have equivalent purpose" was originally mapped to `iframe-title-unique`, but running ACT's own test cases against it exposed that they test different things: ACT's rule accepts a duplicate name when the two iframes point to equivalent content (same resource, mirrors, equivalent ads/sections) and only fails when duplicate-named iframes point to genuinely different content, a content-equivalence judgment call, the same class of check as our existing manual `identical-links-same-purpose`. `iframe-title-unique` instead flags *any* duplicate `title` attribute outright, by design (see its own header comment), a stricter, different, independently-valid check with no ACT counterpart of its own. Moved to "Extra coverage" below; `4b1c6c` itself stays a gap, closing it for real would mean a new manual `identical-iframes-same-purpose`-style rule, not a fix to `iframe-title-unique`.
191
+ - `4b1c6c` "Iframes with identical accessible names have equivalent purpose" was originally mapped to `iframe-title-unique`, but ACT's own test cases showed they test different things: ACT's rule accepts a duplicate name when the frames embed equivalent content (same resource, mirrors, equivalent ads or sections) and fails only when they embed different content, a content-equivalence judgment of the same kind as our manual `identical-links-same-purpose`. `iframe-title-unique` failed any duplicate `title` attribute, which this note first filed as a deliberate, stricter check with no ACT counterpart. `4b1c6c` was then closed by `identical-iframes-same-purpose`. On a second look (#16), the stricter check had no normative basis: WCAG 4.1.2 asks that a frame's name be exposed, not unique, and seven of 4b1c6c's ten passed examples failed under it. A frame's `title` is also its accessible name unless `aria-label` or `aria-labelledby` overrides it, so the two rules asked the same question of the same elements. `iframe-title-unique` is deprecated since 1.8.0 and reports `notApplicable`; `identical-iframes-same-purpose` alone covers `4b1c6c`. See `DESIGN_CHALLENGES.md`.
191
192
  - `5effbb` "Link in context is descriptive" was originally mapped to `link-in-text-block` on a name-similarity guess ("link" + "context/text"); its real applicability/expectation (fetched directly from act-rules.github.io) is "the accessible name together with its programmatically determined link context describes the purpose of the link," WCAG 2.4.4, the *context-aware* sibling of `aizyf1`/`link-name-quality`, unrelated to `link-in-text-block`'s WCAG 1.4.1 color-distinguishability check (which is itself correctly scoped to `a[href]` only, per its own header comment, not a bug). This was later closed by teaching `link-name-quality` to weigh adjacent context; see "Gaps closed since" below.
192
193
  - `oj04fd` "Element in sequential focus order has visible focus" was originally mapped to `css-hidden-focus` on a surface keyword match ("focus," "visible"); its real applicability/expectation is about whether a browser draws *any* visible focus indicator for a normally-visible, normally-positioned element (i.e. `:focus`/`:focus-visible` CSS suppressing the outline with no replacement), a completely different concern from `css-hidden-focus`'s actual check (a keyboard-focusable element that is itself visually hidden by CSS, e.g. `opacity:0`/clip/off-screen). Removed from the matched table and moved to Gaps; a plausible new-rule candidate, not a fix to `css-hidden-focus`, and built as one since, `css-focus-indicator-suppressed`, which is what `oj04fd` maps to now.
193
194
  - `cc0f0a` "Form field label is descriptive" and `c4a8a4` "HTML page title is descriptive" were both mapped to rules that only catch a narrower, adjacent concern: `form-control-programmatic-label-quality` flags a *weak primary labeling mechanism* (title/placeholder used instead of a real label), never whether a properly-associated label's own text is relevant to the field; `page-title-patterns` flags generic/templated title *patterns* (e.g. "Home", "Untitled"), never whether a specific, plausible-looking title actually matches the page's content (ACT's own failed example: `<title>Apple harvesting season</title>` on a page about clementines, a real title/content mismatch neither pattern-list nor mechanism-check was ever designed to catch). Both ACT ids moved to Gaps; both of our rules moved to [Extra coverage](#extra-coverage-beyond-act) as valid, independent, narrower checks with no ACT counterpart of their own. `cc0f0a` has since been picked up by a new rule of its own, `form-control-label-quality`; `c4a8a4` remains a gap.
@@ -235,7 +236,7 @@ The goal is maximum automation, not just parity with ACT's own scope; several ga
235
236
 
236
237
  Rules in this repo with no ACT counterpart, mostly finer decomposition of ACT's single `e086e5` "form field has accessible name" rule into one rule per ARIA widget type, plus some ARIA-validity and structural rules ACT doesn't break out separately:
237
238
 
238
- `aria-hidden-body`, `aria-braille-equivalent`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-prohibited-attr`, `aria-prohibited-children`, `aria-role-name-present`, `binary-control-name-present`, `canvas-text-alternative-present`, `combobox-name-present`, `contrast-computable`, `definition-list-children-valid`, `deprecated-elements-not-used`, `dialog-name-present`, `dlitem-parent-valid`, `duplicate-id-aria`, `embed-text-alternative-present`, `form-control-programmatic-label-quality`, `form-control-single-label`, `iframe-title-unique`, `list-children-valid`, `listbox-name-present`, `listitem-parent-valid`, `meter-name-present`, `nested-interactive-controls-absent`, `option-name-present`, `page-title-patterns`, `progressbar-name-present`, `searchbox-name-present`, `server-side-image-map-absent`, `slider-name-present`, `spinbutton-name-present`, `summary-name-present`, `svg-image-text-alternative-present`, `tab-name-present`, `target-size-minimum`, `td-has-header`, `textbox-name-present`, `tooltip-name-present`, `treeitem-name-present`, `video-poster-text-alternative-present`, `area-alt-present`.
239
+ `aria-hidden-body`, `aria-braille-equivalent`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-prohibited-attr`, `aria-prohibited-children`, `aria-role-name-present`, `binary-control-name-present`, `canvas-text-alternative-present`, `combobox-name-present`, `contrast-computable`, `definition-list-children-valid`, `deprecated-elements-not-used`, `dialog-name-present`, `dlitem-parent-valid`, `duplicate-id-aria`, `embed-text-alternative-present`, `form-control-programmatic-label-quality`, `form-control-single-label`, `list-children-valid`, `listbox-name-present`, `listitem-parent-valid`, `meter-name-present`, `nested-interactive-controls-absent`, `option-name-present`, `page-title-patterns`, `progressbar-name-present`, `searchbox-name-present`, `server-side-image-map-absent`, `slider-name-present`, `spinbutton-name-present`, `summary-name-present`, `svg-image-text-alternative-present`, `tab-name-present`, `target-size-minimum`, `td-has-header`, `textbox-name-present`, `tooltip-name-present`, `treeitem-name-present`, `video-poster-text-alternative-present`, `area-alt-present`.
239
240
 
240
241
  ## Next steps
241
242