@surea11y/core 1.2.0 → 1.4.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +81 -7
  2. package/LICENSE +373 -21
  3. package/README.md +175 -35
  4. package/bin/surea11y-core.js +20 -0
  5. package/docs/API_STABILITY.md +27 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/ENGINE_OPTIONS.md +2 -0
  9. package/docs/I18N.md +12 -9
  10. package/docs/INTEGRATION.md +19 -1
  11. package/docs/LIMITATIONS.md +1 -1
  12. package/docs/OUTPUT_SCHEMA.md +1 -1
  13. package/docs/REPORT.md +1 -1
  14. package/docs/RULE_CATALOG.md +1 -1
  15. package/docs/SARIF.md +59 -0
  16. package/package.json +63 -18
  17. package/src/baseline.js +0 -0
  18. package/src/checks/automatic/area-alt-present.js +63 -31
  19. package/src/checks/automatic/aria-allowed-attr.js +204 -80
  20. package/src/checks/automatic/aria-allowed-role.js +23 -7
  21. package/src/checks/automatic/aria-braille-equivalent.js +34 -10
  22. package/src/checks/automatic/aria-conditional-attr.js +32 -14
  23. package/src/checks/automatic/aria-deprecated-role.js +26 -11
  24. package/src/checks/automatic/aria-hidden-body.js +48 -23
  25. package/src/checks/automatic/aria-hidden-focus.js +420 -66
  26. package/src/checks/automatic/aria-prohibited-attr.js +327 -60
  27. package/src/checks/automatic/aria-prohibited-children.js +111 -103
  28. package/src/checks/automatic/aria-required-attr.js +29 -15
  29. package/src/checks/automatic/aria-required-children.js +44 -24
  30. package/src/checks/automatic/aria-required-parent.js +64 -35
  31. package/src/checks/automatic/aria-role-name-present.js +49 -21
  32. package/src/checks/automatic/aria-roles-valid.js +24 -12
  33. package/src/checks/automatic/aria-valid-attr-value.js +46 -22
  34. package/src/checks/automatic/aria-valid-attr.js +19 -5
  35. package/src/checks/automatic/autocomplete-valid.js +76 -16
  36. package/src/checks/automatic/avoid-inline-spacing.js +23 -8
  37. package/src/checks/automatic/binary-control-name-present.js +62 -50
  38. package/src/checks/automatic/button-name-present.js +54 -24
  39. package/src/checks/automatic/bypass-blocks-present.js +51 -32
  40. package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
  41. package/src/checks/automatic/combobox-name-present.js +40 -45
  42. package/src/checks/automatic/contrast-computable.js +363 -341
  43. package/src/checks/automatic/contrast-enhanced.js +489 -466
  44. package/src/checks/automatic/contrast-minimum.js +488 -465
  45. package/src/checks/automatic/css-orientation-lock.js +51 -35
  46. package/src/checks/automatic/definition-list-children-valid.js +46 -25
  47. package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
  48. package/src/checks/automatic/dialog-name-present.js +47 -85
  49. package/src/checks/automatic/dlitem-parent-valid.js +25 -8
  50. package/src/checks/automatic/duplicate-id-aria.js +28 -9
  51. package/src/checks/automatic/embed-text-alternative-present.js +88 -35
  52. package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
  53. package/src/checks/automatic/form-control-single-label.js +50 -14
  54. package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
  55. package/src/checks/automatic/iframe-focusable-content.js +265 -22
  56. package/src/checks/automatic/iframe-name-present.js +33 -9
  57. package/src/checks/automatic/iframe-title-unique.js +32 -9
  58. package/src/checks/automatic/img-alt-present.js +54 -52
  59. package/src/checks/automatic/input-image-alt-present.js +141 -112
  60. package/src/checks/automatic/label-in-name.js +65 -41
  61. package/src/checks/automatic/language-page-present.js +111 -109
  62. package/src/checks/automatic/link-in-text-block.js +61 -19
  63. package/src/checks/automatic/link-name-present.js +47 -14
  64. package/src/checks/automatic/list-children-valid.js +40 -33
  65. package/src/checks/automatic/listbox-name-present.js +41 -19
  66. package/src/checks/automatic/listitem-parent-valid.js +48 -13
  67. package/src/checks/automatic/menuitem-name-present.js +41 -61
  68. package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
  69. package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
  70. package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
  71. package/src/checks/automatic/meter-name-present.js +40 -36
  72. package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
  73. package/src/checks/automatic/object-text-alternative-present.js +93 -39
  74. package/src/checks/automatic/option-name-present.js +40 -21
  75. package/src/checks/automatic/page-title-present.js +19 -6
  76. package/src/checks/automatic/progressbar-name-present.js +49 -44
  77. package/src/checks/automatic/role-img-alt-present.js +211 -159
  78. package/src/checks/automatic/searchbox-name-present.js +41 -19
  79. package/src/checks/automatic/server-side-image-map-absent.js +27 -11
  80. package/src/checks/automatic/slider-name-present.js +42 -47
  81. package/src/checks/automatic/spinbutton-name-present.js +41 -19
  82. package/src/checks/automatic/summary-name-present.js +39 -17
  83. package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
  84. package/src/checks/automatic/svg-text-alternative-present.js +262 -230
  85. package/src/checks/automatic/tab-name-present.js +39 -60
  86. package/src/checks/automatic/table-headers-attr-valid.js +27 -10
  87. package/src/checks/automatic/table-th-has-data-cells.js +24 -8
  88. package/src/checks/automatic/target-size-minimum.js +123 -48
  89. package/src/checks/automatic/td-has-header.js +53 -12
  90. package/src/checks/automatic/textbox-name-present.js +41 -19
  91. package/src/checks/automatic/tooltip-name-present.js +39 -18
  92. package/src/checks/automatic/treeitem-name-present.js +40 -21
  93. package/src/checks/automatic/valid-lang.js +22 -6
  94. package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
  95. package/src/checks/manual/accesskeys-manual.js +17 -6
  96. package/src/checks/manual/area-alt-decorative-manual.js +194 -193
  97. package/src/checks/manual/area-alt-quality-manual.js +184 -141
  98. package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
  99. package/src/checks/manual/aria-text-manual.js +20 -11
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
  101. package/src/checks/manual/css-hidden-focus.js +375 -169
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
  103. package/src/checks/manual/empty-heading-manual.js +41 -24
  104. package/src/checks/manual/empty-table-header-manual.js +69 -31
  105. package/src/checks/manual/focus-order-semantics-manual.js +60 -13
  106. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
  107. package/src/checks/manual/heading-order-manual.js +50 -8
  108. package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
  109. package/src/checks/manual/image-redundant-alt-manual.js +38 -8
  110. package/src/checks/manual/img-alt-decorative-manual.js +133 -96
  111. package/src/checks/manual/img-alt-quality-manual.js +178 -127
  112. package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
  113. package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
  114. package/src/checks/manual/label-title-only-manual.js +44 -28
  115. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
  116. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
  117. package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
  118. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
  119. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
  120. package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
  121. package/src/checks/manual/landmark-one-main-manual.js +38 -43
  122. package/src/checks/manual/landmark-unique-manual.js +78 -67
  123. package/src/checks/manual/link-name-quality-manual.js +45 -12
  124. package/src/checks/manual/media-transcript-present-manual.js +37 -22
  125. package/src/checks/manual/meta-viewport-large-manual.js +19 -6
  126. package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
  127. package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
  128. package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
  129. package/src/checks/manual/p-as-heading-manual.js +24 -7
  130. package/src/checks/manual/page-has-heading-one-manual.js +42 -32
  131. package/src/checks/manual/page-title-patterns-manual.js +80 -50
  132. package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
  133. package/src/checks/manual/region-manual.js +244 -60
  134. package/src/checks/manual/scope-attr-valid-manual.js +13 -4
  135. package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
  136. package/src/checks/manual/skip-link-manual.js +42 -18
  137. package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
  138. package/src/checks/manual/tabindex-manual.js +13 -4
  139. package/src/checks/manual/table-duplicate-name-manual.js +22 -11
  140. package/src/checks/manual/table-fake-caption-manual.js +48 -10
  141. package/src/checks/manual/video-caption-manual.js +17 -4
  142. package/src/checks/manual-review.js +58 -12
  143. package/src/core.js +41705 -29650
  144. package/src/index.js +2 -0
  145. package/src/report.js +109 -47
  146. package/src/sarif.js +190 -0
  147. package/surea11y.browser.js +37774 -0
  148. package/bin/core.js +0 -348
  149. package/docs/CLI.md +0 -75
  150. package/src/catalogs/composites.wcag.js +0 -490
  151. package/src/checks/rules-and-tags.full.csv +0 -19
  152. package/src/checks/rules-and-tags.full.json +0 -259
  153. package/src/core/aria-helpers.js +0 -970
  154. package/src/core/contrast-helpers.js +0 -1147
  155. package/src/core/dom-helpers.js +0 -4235
  156. package/src/core/dom-runner.js +0 -671
  157. package/src/core/frame-messaging.js +0 -210
  158. package/src/core/frame-scan.js +0 -178
  159. package/src/core/rollup-composites.js +0 -135
  160. package/src/core/rule-meta.js +0 -159
  161. package/src/coverage/wcag-facets.js +0 -1079
  162. package/src/coverage/wcag-version-map.js +0 -84
  163. package/src/i18n/en.js +0 -923
  164. package/src/i18n/fr.js +0 -844
  165. package/src/policy/contracts.js +0 -18
  166. package/src/policy/resolvePolicy.js +0 -55
  167. package/src/policy/schemas/engine-options.schema.json +0 -103
  168. package/src/policy/schemas/policy-contract.schema.json +0 -40
package/README.md CHANGED
@@ -1,33 +1,57 @@
1
1
  # @surea11y/core
2
2
 
3
- > **Reliable accessibility testing for real web applications.**
3
+ [![surea11y core](docs/assets/brand-tag-dark.svg#gh-dark-mode-only)](https://www.npmjs.com/package/@surea11y/core#gh-dark-mode-only)
4
+ [![surea11y core](docs/assets/brand-tag-light.svg#gh-light-mode-only)](https://www.npmjs.com/package/@surea11y/core#gh-light-mode-only)
5
+ [![npm](https://img.shields.io/npm/v/@surea11y/core?style=flat-square&label=npm&labelColor=101413&color=3A4441)](https://www.npmjs.com/package/@surea11y/core)
6
+ [![node](https://img.shields.io/node/v/@surea11y/core?style=flat-square&label=node&labelColor=101413&color=3A4441)](package.json)
7
+ [![license](https://img.shields.io/badge/license-MPL--2.0-3A4441?style=flat-square&labelColor=101413)](LICENSE)
4
8
 
5
- surea11y is an accessibility engine designed to help development teams
6
- identify objective accessibility issues early in the software lifecycle.
7
- It runs against either static HTML or fully rendered browser pages,
8
- producing deterministic, standards-traceable results that are suitable
9
- for local development, automated testing and CI/CD pipelines.
9
+ > **Accessibility testing that tells you what it can't tell you.**
10
10
 
11
- Unlike browser extensions or cloud-based services, surea11y is a
12
- library-first project. You install it, run it where your code runs, and
13
- receive structured results that can be consumed by people, scripts or
14
- reporting tools.
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.
15
14
 
16
- ## Why surea11y?
15
+ *Sure* means certainty about what is known, and honesty about what isn't.
17
16
 
18
- Accessibility automation is only valuable if developers can trust its
19
- results.
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
20
 
21
- surea11y is built around a conservative philosophy: **never report
22
- certainty when certainty cannot be established objectively.**
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.
23
24
 
24
- Instead of relying on heuristics that may generate false positives, each
25
- rule makes a single deterministic decision. If a violation can be
26
- proven, the outcome is `fail`. If human judgement is required, the
27
- engine reports `cantTell` instead of guessing.
25
+ ## What automated testing can and cannot do
28
26
 
29
- This makes the engine predictable, reproducible and suitable for
30
- automated quality gates.
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:
33
+
34
+ - **`fail`** — a violation provable from the DOM. Reserved for objective,
35
+ normative cases.
36
+ - **`pass`** — this rule's specific condition is met. Not a claim that the page
37
+ is accessible.
38
+ - **`cantTell`** — a human has to decide this, and the result says what was
39
+ ambiguous.
40
+ - **`notApplicable`** — the rule's precondition isn't present.
41
+
42
+ `cantTell` is the point of the project. An engine that quietly discards what it
43
+ cannot determine produces a shorter report and a false sense of coverage.
44
+
45
+ ## What this engine does not detect
46
+
47
+ Keyboard traps, reflow and clipping at 400% zoom, anything that only exists
48
+ after a click or an async load, and judgement calls such as whether a heading is
49
+ meaningful — these lie outside what a static DOM scan can establish. Each is a
50
+ reasoned decision rather than an oversight.
51
+
52
+ [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md) lists them in full with the
53
+ reasoning for each. A `pass` from this engine — or from any automated tool — is
54
+ never a substitute for the manual review WCAG itself requires.
31
55
 
32
56
  ### Key principles
33
57
 
@@ -44,7 +68,9 @@ automated quality gates.
44
68
  - **Extensible.** Add custom rules, register policies and filter scans
45
69
  by rule IDs, tags or WCAG version.
46
70
  - **Localized reporting.** Human-readable messages can be translated
47
- without affecting machine-readable data.
71
+ without affecting machine-readable data. Ships with `en`, `fr`, `de`,
72
+ and `es` today — see [`docs/I18N.md`](./docs/I18N.md) to use one or
73
+ contribute another.
48
74
 
49
75
  ---
50
76
 
@@ -92,6 +118,26 @@ For a detailed comparison of both execution models, see
92
118
 
93
119
  ---
94
120
 
121
+ ## Which package do I need?
122
+
123
+ surea11y is a family of packages sharing one engine. Install the one that
124
+ matches how you test — each pulls in `@surea11y/core` for you.
125
+
126
+ | I want to… | Install |
127
+ |---|---|
128
+ | Add accessibility checks to **Playwright** tests | [`@surea11y/playwright`](https://github.com/SureA11y/playwright#readme) |
129
+ | …**Puppeteer** | [`@surea11y/puppeteer`](https://github.com/SureA11y/puppeteer#readme) |
130
+ | …**Selenium** | [`@surea11y/selenium`](https://github.com/SureA11y/selenium#readme) |
131
+ | …**Cypress** | [`@surea11y/cypress`](https://github.com/SureA11y/cypress#readme) |
132
+ | …**WebdriverIO** | [`@surea11y/webdriverio`](https://github.com/SureA11y/webdriverio#readme) |
133
+ | Assert in **Jest or Vitest** component tests | [`@surea11y/test-matchers`](https://github.com/SureA11y/test-matchers#readme) |
134
+ | Scan static HTML from a **terminal or CI pipeline** | [`@surea11y/cli`](https://github.com/SureA11y/cli#readme) |
135
+ | Run the engine against **a DOM I already have** | `@surea11y/core` (this package) |
136
+
137
+ The rest of this README covers `@surea11y/core` itself.
138
+
139
+ ---
140
+
95
141
  ## Installation
96
142
 
97
143
  Install the core package from npm:
@@ -100,10 +146,13 @@ Install the core package from npm:
100
146
  npm install @surea11y/core
101
147
  ```
102
148
 
103
- The core engine has no runtime dependencies — requiring the library
104
- itself never loads `jsdom`. Only the bundled CLI loads it, and only
105
- when you actually run a scan, keeping the library lightweight and
106
- suitable for embedding into your own tooling.
149
+ The core engine has **zero runtime dependencies**. Installing it pulls
150
+ nothing else into your tree, which keeps it lightweight and suitable for
151
+ embedding into your own tooling.
152
+
153
+ The engine needs a DOM to read, but it never creates one — you supply it,
154
+ whether that's jsdom, a Playwright page, or the live document in a
155
+ browser. That is why nothing is installed on your behalf.
107
156
 
108
157
  ---
109
158
 
@@ -111,18 +160,21 @@ suitable for embedding into your own tooling.
111
160
 
112
161
  ### CLI
113
162
 
114
- The CLI is the fastest way to analyse a page without writing any code.
163
+ The CLI ships as a separate package, [`@surea11y/cli`](https://www.npmjs.com/package/@surea11y/cli),
164
+ so that installing the engine never pulls a DOM implementation into
165
+ projects that already have one:
115
166
 
116
167
  ```bash
117
- npx @surea11y/core scan ./index.html
118
- npx @surea11y/core scan https://example.com/
168
+ npx @surea11y/cli scan ./index.html
169
+ npx @surea11y/cli scan https://example.com/
119
170
  ```
120
171
 
121
172
  The CLI analyses static HTML. It does not execute client-side
122
173
  JavaScript, making it ideal for static sites and server-rendered
123
174
  applications.
124
175
 
125
- For available options, exit codes and advanced usage, see `docs/CLI.md`.
176
+ For available options, exit codes and advanced usage, see the
177
+ [CLI documentation](https://github.com/SureA11y/cli#readme).
126
178
 
127
179
  ---
128
180
 
@@ -133,7 +185,12 @@ surea11y exposes two entry points depending on where your code executes.
133
185
  #### Node.js + jsdom
134
186
 
135
187
  Use `runDomRulesInPage()` when your application already has a DOM
136
- available through jsdom.
188
+ available through jsdom. jsdom is not a dependency of this package, so
189
+ install it alongside if you don't already have it:
190
+
191
+ ```bash
192
+ npm install jsdom
193
+ ```
137
194
 
138
195
  ```js
139
196
  const { JSDOM } = require("jsdom");
@@ -198,6 +255,66 @@ For complete integration examples, see `docs/INTEGRATION.md`.
198
255
 
199
256
  ---
200
257
 
258
+ #### Standalone browser bundle
259
+
260
+ For a page that isn't driven by an automation framework at all — a manual
261
+ QA pass, a bookmarklet, a browser extension — the package also ships a
262
+ self-contained bundle that needs no `require`, no bundler, and no build
263
+ step:
264
+
265
+ ```html
266
+ <script src="node_modules/@surea11y/core/surea11y.browser.js"></script>
267
+ <script>
268
+ const result = a11ycore.runa11yCoreInPage(
269
+ location.href, // pageUrl
270
+ null, // contextSelector
271
+ {}, // engineOptions
272
+ null // runOnly
273
+ );
274
+
275
+ console.log(result.checksResults.filter(r => r.outcome === "fail"));
276
+ </script>
277
+ ```
278
+
279
+ Loading `surea11y.browser.js` defines a single global, `a11ycore`, exposing
280
+ the same `runa11yCoreInPage` function described above — calling it runs a
281
+ real scan against the page it's loaded into and returns the same result
282
+ shape documented in [Understanding the Results](#understanding-the-results).
283
+
284
+ `contextSelector`, `engineOptions`, and `runOnly` are the same three
285
+ arguments described throughout this README and `docs/ENGINE_OPTIONS.md` —
286
+ nothing about calling the engine changes just because it's loaded this
287
+ way. A scan scoped to one region, filtered to a specific WCAG level, with
288
+ a couple of known-noisy selectors excluded, looks like this:
289
+
290
+ ```html
291
+ <script src="node_modules/@surea11y/core/surea11y.browser.js"></script>
292
+ <script>
293
+ const result = a11ycore.runa11yCoreInPage(
294
+ location.href,
295
+ "#main", // contextSelector: scan only this region
296
+ {
297
+ excludeSelectors: ["#cookie-banner", ".intercom-launcher"],
298
+ contrast: { mode: "auditorAssist" } // trade some false-positive protection for more findings
299
+ },
300
+ { tags: ["wcag2a", "wcag2aa"] } // runOnly: WCAG 2.0 A/AA rules only
301
+ );
302
+
303
+ console.log(result.checksResults.filter(r => r.outcome === "fail"));
304
+ </script>
305
+ ```
306
+
307
+ This bundle is generated from the same rule sources as the rest of the
308
+ engine (`npm run build` regenerates it alongside `src/core.js`), so its
309
+ behavior never drifts from the library's. It intentionally exposes only
310
+ `runa11yCoreInPage` — cross-frame scanning
311
+ (`runa11yCoreAcrossFrames`/`a11yCoreEnableFrameResponder`) requires the
312
+ embedded frame to also load the engine and opt in, which doesn't fit a
313
+ single dropped-in `<script>` tag; reach for the npm package directly if
314
+ you need that.
315
+
316
+ ---
317
+
201
318
  ## Understanding the Results
202
319
 
203
320
  Every scan returns a structured JSON document designed for both
@@ -271,9 +388,10 @@ and progressively explore more advanced features.
271
388
  |---|---|
272
389
  | `docs/OUTPUT_SCHEMA.md` | Complete description of every field returned by the engine. |
273
390
  | `docs/API_STABILITY.md` | Semver guarantees on the result shape, and the rule-ID deprecation policy. |
274
- | `docs/CLI.md` | CLI commands, options, exit codes and examples. |
275
391
  | `docs/BASELINE.md` | CI baseline/allowlist: gate builds only on new violations. |
276
392
  | `docs/REPORT.md` | Self-contained HTML report: browsable summary, WCAG rollup, filterable occurrence table. |
393
+ | `docs/SARIF.md` | SARIF 2.1.0 report for GitHub Code Scanning and other SARIF dashboards. |
394
+ | `docs/CI_INTEGRATIONS.md` | GitHub Actions and Bitbucket Pipelines templates wrapping the CLI. |
277
395
  | `docs/ENGINE_OPTIONS.md` | Configuration, filtering, policies and localization. |
278
396
  | `docs/INTEGRATION.md` | Using surea11y with jsdom, Playwright, Puppeteer, Selenium, Cypress and other drivers. |
279
397
  | `docs/BINDING_AUTHORS_GUIDE.md` | Building new framework integrations on top of the engine. |
@@ -341,8 +459,7 @@ The repository is organised so that the accessibility engine, rule
341
459
  implementations and supporting infrastructure remain clearly separated.
342
460
 
343
461
  ```text
344
- bin/
345
- core.js # CLI entry point
462
+ surea11y.browser.js # Generated standalone browser bundle
346
463
 
347
464
  src/
348
465
  index.js # Public API
@@ -421,12 +538,35 @@ supported versions and the preferred disclosure process.
421
538
 
422
539
  ---
423
540
 
541
+ ## Versioning & stability
542
+
543
+ `@surea11y/core` follows [semantic versioning](https://semver.org/). The result
544
+ shape is a written contract — see [`docs/API_STABILITY.md`](docs/API_STABILITY.md)
545
+ for exactly which fields are covered by semver, what triggers a patch/minor/major
546
+ bump, the release cadence, and the rule-ID deprecation policy.
547
+
548
+ In short: patch and minor releases are always backward-compatible, so a consumer
549
+ pinned to a `^1.y.0` range is never broken by an upgrade within the `1.x` line.
550
+ Correctness fixes ship as patches when ready; feature work is batched into
551
+ periodic minors; breaking changes are reserved for major versions and are rare by
552
+ design.
553
+
554
+ ## Maintainer
555
+
556
+ surea11y is built and maintained by [Jorge Rumoroso](https://github.com/rumoroso).
557
+
558
+ Bug reports and rule proposals are welcome via
559
+ [issues](https://github.com/SureA11y/core/issues). For security disclosures see
560
+ [`SECURITY.md`](./SECURITY.md).
561
+
424
562
  ## License
425
563
 
426
- This project is released under the MIT License.
564
+ This project is released under the Mozilla Public License 2.0 (MPL-2.0).
427
565
 
428
566
  See the accompanying `LICENSE` file for the complete license text.
429
567
 
568
+ MPL-2.0 is file-level copyleft: it applies to `@surea11y/core`'s own source files, not to code that merely depends on it. A project that installs `@surea11y/core` as a normal package dependency and imports its public API — without copying or modifying this repository's source files — is unaffected by MPL-2.0 and may keep its own license (including a permissive one like MIT).
569
+
430
570
  ---
431
571
 
432
572
  ## Final Notes
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /* SPDX-License-Identifier: MPL-2.0 */
3
+
4
+ 'use strict';
5
+
6
+ // Redirects the pre-1.4.0 `npx @surea11y/core scan ...` that older docs still
7
+ // show. Not named `surea11y`: that belongs to @surea11y/cli, and core is a
8
+ // transitive dependency of every binding, so both would collide in one .bin.
9
+
10
+ process.stderr.write(
11
+ `The surea11y CLI is no longer part of @surea11y/core (moved in 1.4.0).
12
+
13
+ npx @surea11y/cli scan <file-or-url>
14
+
15
+ Install it with: npm install --save-dev @surea11y/cli
16
+ Docs: https://github.com/SureA11y/cli
17
+ `
18
+ );
19
+
20
+ process.exit(2);
@@ -1,6 +1,6 @@
1
1
  # API stability & versioning
2
2
 
3
- `@surea11y/core` has real downstream consumers today (5 framework bindings — Playwright, Puppeteer, Selenium, WebdriverIO, Cypress — plus a Jest/Vitest matcher package), all pinned to a `^1.1.0`-style semver range. Until now, "what counts as a breaking change" was implicit — discoverable only by reading source, not written down anywhere. This document makes that contract explicit.
3
+ `@surea11y/core` has real downstream consumers today: 5 first-party framework bindings (Playwright, Puppeteer, Selenium, WebdriverIO, Cypress) published to npm, plus a Jest/Vitest matcher (`@surea11y/test-matchers`, `toHaveNoA11yViolations()`) — all pinned to a `^1.1.0`-style semver range. Until now, "what counts as a breaking change" was implicit — discoverable only by reading source, not written down anywhere. This document makes that contract explicit.
4
4
 
5
5
  ## Stable fields (covered by semver)
6
6
 
@@ -13,6 +13,22 @@ Removing, renaming, or changing the type/meaning of any of these is a **major**
13
13
 
14
14
  This list is deliberately not a new, invented guarantee — it codifies what the 6 real consumers above (and `docs/OUTPUT_SCHEMA.md`'s own worked examples) already depend on today, either directly or as documented shape.
15
15
 
16
+ ## Package entry points (covered by semver)
17
+
18
+ Since 1.4.0 the package declares an explicit `exports` map. These are the only importable paths, and removing or repointing one is a **major** bump:
19
+
20
+ | Specifier | Resolves to | Contents |
21
+ |---|---|---|
22
+ | `@surea11y/core` | `src/index.js` | the full engine surface (`runDomRulesInPage`, `runa11yCoreInPage`, catalog accessors, …) |
23
+ | `@surea11y/core/baseline` | `src/baseline.js` | `buildBaselineEntries()`, `matchBaseline()` |
24
+ | `@surea11y/core/report` | `src/report.js` | `renderHtmlReport()` |
25
+ | `@surea11y/core/sarif` | `src/sarif.js` | `renderSarifReport()` |
26
+ | `@surea11y/core/browser` | `surea11y.browser.js` | the standalone browser bundle, for bundlers that resolve it as a module |
27
+
28
+ Anything **not** in that table — `src/core/*`, `src/checks/*`, `src/i18n/*`, `src/policy/*`, and the generated `src/core.js` itself — is internal. Before 1.4.0 there was no `exports` map, so those paths were technically reachable via deep `require()`; they were never documented as public and are no longer resolvable. The `<script src="node_modules/@surea11y/core/surea11y.browser.js">` form documented in the README is a filesystem path, not module resolution, and is unaffected.
29
+
30
+ Declaring this map is what lets the engine's internal file layout change without a major bump. Note that `src/checks/*` is still *shipped* (the generated bundle `require()`s it at runtime) — shipped is not the same as public.
31
+
16
32
  ## Explicitly unstable (not covered by semver)
17
33
 
18
34
  - `perfStats` and `ruleTimings` — internal timing/debug counters, only present when `engineOptions.perfStats`/`.profileRules` is set. Shape not covered by this document.
@@ -27,6 +43,16 @@ This list is deliberately not a new, invented guarantee — it codifies what the
27
43
 
28
44
  `engine.schemaVersion` has been `"1.0.0"` since the engine's first release and has never needed a bump — nothing has changed a stable field's shape yet. Adding the `deprecated`/`deprecation` meta fields described below is purely additive (new optional fields, ignored safely by anything not looking for them), so it does **not** warrant a schema bump either — this is the policy's first real application.
29
45
 
46
+ ## Release cadence
47
+
48
+ The version number is the contract — not a measure of how much has changed or how often. surea11y follows semver strictly, so what a bump *means* is fixed regardless of how frequently they happen:
49
+
50
+ - **Patch (`x.y.Z`)** — rule-correctness fixes and documentation updates. Released promptly, as needed, rather than held back; always safe to adopt within a major line.
51
+ - **Minor (`x.Y.0`)** — additive, backward-compatible work: new rules, new locales, new `engineOptions`, new output formats. Batched into periodic releases rather than shipped one change at a time.
52
+ - **Major (`X.0.0`)** — a breaking change to a stable field (see above). Rare by design; the entire point of the stable-fields list is to keep these infrequent and well-signposted.
53
+
54
+ Because every `1.x` release is backward-compatible, a consumer pinned to a `^1.y.0` range is never broken by an upgrade within the line — so a steady stream of patch/minor releases reflects active maintenance and prompt fixes, not instability. Frequency of releases is not a signal of churn; a change to a **major** version is.
55
+
30
56
  ## Rule-ID deprecation policy
31
57
 
32
58
  A rule can be marked deprecated in its own `meta`:
@@ -1,8 +1,8 @@
1
1
  # Binding authors' guide
2
2
 
3
- Written for whoever builds the *next* framework binding on top of `surea11y` (Puppeteer, Cypress, Selenium, WebdriverIO, whatever comes after) — not for someone consuming a binding, and not for someone calling the engine directly. If that's you, see [`INTEGRATION.md`](./INTEGRATION.md) instead.
3
+ Written for whoever builds the *next* framework binding on top of `@surea11y/core` (Puppeteer, Cypress, Selenium, WebdriverIO, whatever comes after) — not for someone consuming a binding, and not for someone calling the engine directly. If that's you, see [`INTEGRATION.md`](./INTEGRATION.md) instead.
4
4
 
5
- The first real binding, `surea11y-playwright` (a sibling project, not part of this repo), has already worked through most of the design questions a new binding hits. This doc exists so the next one doesn't have to re-derive them — it's a checklist and a map of "what the engine already gives you for free" vs. "what every binding has to build itself," backed by what that binding actually did, not theory.
5
+ The first real binding, `@surea11y/playwright` (a sibling project, not part of this repo), has already worked through most of the design questions a new binding hits. This doc exists so the next one doesn't have to re-derive them — it's a checklist and a map of "what the engine already gives you for free" vs. "what every binding has to build itself," backed by what that binding actually did, not theory.
6
6
 
7
7
  ## Core-engine vs. binding-layer: know which side you're on
8
8
 
@@ -10,19 +10,19 @@ Some engine-parity features (relative to other established engines) are **engine
10
10
 
11
11
  **Already engine-level, works the moment you call `runa11yCoreInPage`/`runDomRulesInPage` — no binding code needed:**
12
12
  - All rule execution, WCAG SC mapping, composite rollups.
13
- - `runOnly`/`engineOptions.rules`/`.tags`/`.tests` rule selection — including [WCAG-version filtering](./ENGINE_OPTIONS.md#filtering-by-wcag-version-21-vs-22) (`wcag21a`/`wcag22aa`-style tags): if your binding has any kind of `.withTags()`/`.options()` passthrough that forwards `runOnly`/`engineOptions` generically, WCAG-version filtering already works through it with zero extra code — just document the tag vocabulary for your users, the way `surea11y-playwright`'s README does.
13
+ - `runOnly`/`engineOptions.rules`/`.tags`/`.tests` rule selection — including [WCAG-version filtering](./ENGINE_OPTIONS.md#filtering-by-wcag-version-21-vs-22) (`wcag21a`/`wcag22aa`-style tags): if your binding has any kind of `.withTags()`/`.options()` passthrough that forwards `runOnly`/`engineOptions` generically, WCAG-version filtering already works through it with zero extra code — just document the tag vocabulary for your users, the way `@surea11y/playwright`'s README does.
14
14
  - `structuralPath` on every `fail`/`cantTell` occurrence — if your binding passes occurrences through unreshaped (don't strip fields you don't recognize), this reaches your consumers automatically.
15
15
  - `engineOptions.customRules` — runtime rule registration. Works through a generic `engineOptions` passthrough too, **but** see the caveat below: if your binding crosses a serialization boundary (see next section), your consumers must pass `runInPage`/`applicability` as `fn.toString()` source, not a live function. Worth a dedicated `.withCustomRules([...])` convenience method for ergonomics, but not required for the feature to work.
16
- - Cross-frame scanning **if** your driver reaches every frame itself already (Puppeteer/Cypress/Selenium all can, via CDP or equivalent) — you don't need `runa11yCoreAcrossFrames`/`a11yCoreEnableFrameResponder` at all. Just call the engine once per frame your driver already gives you and merge the results yourself (see `surea11y-playwright`'s `.frames(true)`, which does exactly this — no engine change was needed for it). The `postMessage`-based cross-frame functions exist specifically for the *no-automation-driver* case (a plain injected script) and are the wrong tool for a driver-based binding.
16
+ - Cross-frame scanning **if** your driver reaches every frame itself already (Puppeteer/Cypress/Selenium all can, via CDP or equivalent) — you don't need `runa11yCoreAcrossFrames`/`a11yCoreEnableFrameResponder` at all. Just call the engine once per frame your driver already gives you and merge the results yourself (see `@surea11y/playwright`'s `.frames(true)`, which does exactly this — no engine change was needed for it). The `postMessage`-based cross-frame functions exist specifically for the *no-automation-driver* case (a plain injected script) and are the wrong tool for a driver-based binding.
17
17
 
18
18
  **Binding-layer — your binding has to build these itself, the engine won't:**
19
- - **Element references.** The engine returns `selector`/`structuralPath` strings, never a live handle — it has no concept of your driver's element-reference type. Resolve `occurrences[i].selector` back to a real handle yourself (Playwright's approach: `page.evaluateHandle` instead of `page.evaluate`, then `elementHandle.$(selector)` per occurrence — see `.elementRef(true)` in `surea11y-playwright`).
20
- - **Result verbosity/reporter filtering.** The engine deliberately always returns every rule's outcome, including `pass`/`notApplicable` — "not a violations-only list" is a stated engine design choice (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md)), not an oversight to work around. If your consumers want a trimmed view for CI-scale output, that's a post-filter your binding adds (`surea11y-playwright`'s `.reportOnly(['fail','cantTell'])` is a simple array-filter over the full result — no engine change).
19
+ - **Element references.** The engine returns `selector`/`structuralPath` strings, never a live handle — it has no concept of your driver's element-reference type. Resolve `occurrences[i].selector` back to a real handle yourself (Playwright's approach: `page.evaluateHandle` instead of `page.evaluate`, then `elementHandle.$(selector)` per occurrence — see `.elementRef(true)` in `@surea11y/playwright`).
20
+ - **Result verbosity/reporter filtering.** The engine deliberately always returns every rule's outcome, including `pass`/`notApplicable` — "not a violations-only list" is a stated engine design choice (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md)), not an oversight to work around. If your consumers want a trimmed view for CI-scale output, that's a post-filter your binding adds (`@surea11y/playwright`'s `.reportOnly(['fail','cantTell'])` is a simple array-filter over the full result — no engine change).
21
21
  - **Formatted failure output for your framework's own assertion/reporting style** (e.g. Playwright/Jest-style multi-line failure messages). The engine's raw result is framework-agnostic on purpose; shaping it into "what shows up in a failed test's stack trace" is squarely binding territory.
22
22
 
23
23
  ## The serialization-boundary caveat
24
24
 
25
- If your binding drives a *separate JS realm* (a browser page/tab is a different realm than your Node test process — this is Playwright/Puppeteer/Selenium's situation, not Cypress's, since Cypress test code already runs in-browser), anything you hand to `page.evaluate()`-equivalent gets structurally cloned/JSON-serialized. **Live functions do not survive that boundary.** This bit `surea11y-playwright` in two places:
25
+ If your binding drives a *separate JS realm* (a browser page/tab is a different realm than your Node test process — this is Playwright/Puppeteer/Selenium's situation, not Cypress's, since Cypress test code already runs in-browser), anything you hand to `page.evaluate()`-equivalent gets structurally cloned/JSON-serialized. **Live functions do not survive that boundary.** This bit `@surea11y/playwright` in two places:
26
26
  1. `runa11yCoreInPage` itself is designed around this — it's fully self-contained (`.toString()`-serializable, no closure over outer scope) specifically so it can be reconstructed from source inside the page realm.
27
27
  2. `engineOptions.customRules[].runInPage`/`.applicability` accept a function-source string for exactly this reason — a binding crossing this boundary must tell its consumers to pass `fn.toString()`, not `fn`. Document this prominently; it's an easy trap (a function looks like it should just work as an argument until it silently fails to serialize).
28
28
 
@@ -30,7 +30,7 @@ If your binding runs in the *same* realm as the page (a browser extension conten
30
30
 
31
31
  ## Things to check before shipping a new binding
32
32
 
33
- A short list, derived from what the audit pass on `surea11y-playwright` actually found missing on a first pass (per its own `ROADMAP.md`) — worth checking explicitly rather than assuming your binding's generic passthrough covers them:
33
+ A short list, derived from what the audit pass on `@surea11y/playwright` actually found missing on a first pass (per its own `ROADMAP.md`) — worth checking explicitly rather than assuming your binding's generic passthrough covers them:
34
34
  - [ ] Combinations of your own filtering methods behave sanely together (e.g. include+exclude on the same ID, tag-include + tag-exclude on the same tag) — these interact through the engine's `includeMode`/exclude-always-wins semantics ([`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md)), test them explicitly rather than assuming.
35
35
  - [ ] `structuralPath` and `customRules` still work correctly when combined with whatever binding-layer features you build (verbosity filtering, element refs, per-frame scanning) — a filter applied after the fact should never silently drop fields a consumer expects on a surviving occurrence.
36
36
  - [ ] If you support cross-frame scanning via your own driver, confirm each frame's result gets the same normalization (selector/structuralPath/severity) as a single-document scan — don't let a "per-frame" code path silently skip the shared result-shaping logic.
@@ -38,4 +38,4 @@ A short list, derived from what the audit pass on `surea11y-playwright` actually
38
38
 
39
39
  ## A known engine-side tradeoff worth knowing about
40
40
 
41
- `src/core.js` is not small (~3.1MB as of 2026-07-22) because the bundler-free, no-driver-context functions (`runa11yCoreInPage`, `runa11yCoreAcrossFrames`, `a11yCoreEnableFrameResponder`) each carry their own complete self-contained copy of the rule catalog. If your binding only ever uses `require('@surea11y/core')` in Node and injects `runa11yCoreInPage.toString()` into the page (the same pattern `surea11y-playwright` uses), your actual browser-injected payload is unaffected by this — only your Node-side `require()` footprint grows. Worth knowing if your binding's own package size matters to your consumers.
41
+ `src/core.js` is not small (~3.1MB as of 2026-07-22) because the bundler-free, no-driver-context functions (`runa11yCoreInPage`, `runa11yCoreAcrossFrames`, `a11yCoreEnableFrameResponder`) each carry their own complete self-contained copy of the rule catalog. If your binding only ever uses `require('@surea11y/core')` in Node and injects `runa11yCoreInPage.toString()` into the page (the same pattern `@surea11y/playwright` uses), your actual browser-injected payload is unaffected by this — only your Node-side `require()` footprint grows. Worth knowing if your binding's own package size matters to your consumers.
@@ -0,0 +1,103 @@
1
+ # CI/CD pipeline integrations
2
+
3
+ Ready-to-paste templates wrapping the [CLI](https://github.com/SureA11y/cli#readme) (`npx @surea11y/cli scan ...`) in GitHub Actions and Bitbucket Pipelines. If you're calling the library directly from your own Node script instead of the CLI, see [`INTEGRATION.md`](./INTEGRATION.md#ci-gating-a-build-on-the-result) instead — this page is specifically about the CLI as a pipeline step.
4
+
5
+ All of these rely on the CLI's own exit codes (`0` clean, `1` at least one — or one *new*, with `--baseline` — `fail` outcome, `2` a usage/scan error) to gate the pipeline; no extra scripting is required for basic pass/fail gating.
6
+
7
+ ## GitHub Actions
8
+
9
+ ### Basic: gate on exit code
10
+
11
+ ```yaml
12
+ name: Accessibility scan
13
+ on: [pull_request]
14
+
15
+ jobs:
16
+ a11y-scan:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-node@v4
21
+ with:
22
+ node-version: 20
23
+ - run: npm ci && npm run build # produce whatever static HTML you're scanning
24
+ - run: npx @surea11y/cli scan ./dist/index.html
25
+ ```
26
+
27
+ This fails the job the moment any `fail` outcome is found. For an existing site with pre-existing violations, see the baseline variant below instead of disabling the step.
28
+
29
+ ### With a baseline (existing site, gate only on new violations)
30
+
31
+ ```sh
32
+ # Once, locally: record every current fail occurrence, commit the file.
33
+ npx @surea11y/cli scan ./dist/index.html --write-baseline a11y-baseline.json
34
+ git add a11y-baseline.json
35
+ ```
36
+
37
+ ```yaml
38
+ - run: npm ci && npm run build
39
+ - run: npx @surea11y/cli scan ./dist/index.html --baseline a11y-baseline.json
40
+ ```
41
+
42
+ See [`BASELINE.md`](./BASELINE.md) for what counts as "known" vs. "new", and how to regenerate the file as violations get fixed.
43
+
44
+ ### Uploading SARIF to GitHub Code Scanning
45
+
46
+ `upload-sarif` needs `security-events: write` permission and does not fail the job itself — pair it with a separate gating step (or `continue-on-error` + your own check) if you also want the build to fail on new violations.
47
+
48
+ ```yaml
49
+ name: Accessibility scan
50
+ on: [pull_request]
51
+
52
+ permissions:
53
+ contents: read
54
+ security-events: write
55
+
56
+ jobs:
57
+ a11y-scan:
58
+ runs-on: ubuntu-latest
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ - uses: actions/setup-node@v4
62
+ with:
63
+ node-version: 20
64
+ - run: npm ci && npm run build
65
+ - name: Scan (report, don't fail the job here)
66
+ run: npx @surea11y/cli scan ./dist/index.html --baseline a11y-baseline.json --sarif results.sarif
67
+ continue-on-error: true
68
+ id: scan
69
+ - name: Upload SARIF to Code Scanning
70
+ uses: github/codeql-action/upload-sarif@v3
71
+ with:
72
+ sarif_file: results.sarif
73
+ - name: Fail the job on new violations
74
+ if: steps.scan.outcome == 'failure'
75
+ run: exit 1
76
+ ```
77
+
78
+ See [`SARIF.md`](./SARIF.md) for the output format and a known limitation: a scan of a **local file** in your checkout gets inline annotations in the Code Scanning UI; a scan of a **live URL** doesn't (SARIF/Code Scanning can only associate a finding with a real file in the repository).
79
+
80
+ ## Bitbucket Pipelines
81
+
82
+ Bitbucket Pipelines has no built-in SARIF-consuming dashboard equivalent to GitHub Code Scanning, so the CLI's exit code (plus, optionally, `--html` as a downloadable pipeline artifact) is the practical integration point rather than `--sarif`.
83
+
84
+ ```yaml
85
+ pipelines:
86
+ pull-requests:
87
+ '**':
88
+ - step:
89
+ name: Accessibility scan
90
+ image: node:20
91
+ script:
92
+ - npm ci
93
+ - npm run build
94
+ - npx @surea11y/cli scan ./dist/index.html --baseline a11y-baseline.json --html a11y-report.html
95
+ artifacts:
96
+ - a11y-report.html
97
+ ```
98
+
99
+ The step fails the pipeline on the CLI's exit code exactly like any other `script` entry; `a11y-report.html` (see [`REPORT.md`](./REPORT.md)) is attached as a downloadable build artifact so a reviewer can open it without re-running the scan locally.
100
+
101
+ ## Free-tier/private-repo minute limits
102
+
103
+ If your pipeline provider's free tier is minute-limited (Bitbucket Pipelines' free tier is 50 build-minutes/month on private workspaces, for example), a `jsdom`-based scan of static/server-rendered HTML (what the CLI does) is far cheaper than driving a real browser — see [the CLI docs](https://github.com/SureA11y/cli/blob/main/docs/CLI.md#what-it-can-and-cant-scan) for what that trades away (no client-rendered content, no real CSS layout).
@@ -224,6 +224,8 @@ See the option-by-option table above for anything not shown here, and the `custo
224
224
 
225
225
  Every shipped rule is baked into `src/core.js` at build time. `engineOptions.customRules` is the runtime escape hatch: an array of rule descriptors registered for that one call only — nothing is added to the static catalog (`getRulesCatalog()`/`getChecksCatalog()`), and nothing persists between calls. This is deliberate, not a limitation to work around: surea11y already takes fresh `engineOptions` per call with no mutable global config (unlike some other engines, which need a `configure()`/`reset()` step against a shared runtime), and custom rules follow that same per-call model.
226
226
 
227
+ Calling the library directly is one way in; the CLI also exposes this via `--custom-rules <path>` (a local file, loaded once per scan) — see [the CLI docs](https://github.com/SureA11y/cli/blob/main/docs/CLI.md#custom-rules).
228
+
227
229
  A descriptor has the *same shape as an internal rule module's own export* — if you already know how to write a rule file for this engine, you already know this API:
228
230
 
229
231
  ```js
package/docs/I18N.md CHANGED
@@ -6,10 +6,12 @@ Every rule's title/description, and every occurrence's summary/hint, is localize
6
6
 
7
7
  | Locale | File | Keys | Coverage vs. English |
8
8
  |---|---|---|---|
9
- | `en` (English) | `src/i18n/en.js` | 600 | 100% (the canonical/fallback set) |
10
- | `fr` (French) | `src/i18n/fr.js` | 600 | 100% |
9
+ | `en` (English) | `src/i18n/en.js` | 614 | 100% (the canonical/fallback set) |
10
+ | `fr` (French) | `src/i18n/fr.js` | 614 | 100% |
11
+ | `de` (German) | `src/i18n/de.js` | 614 | 100% |
12
+ | `es` (Spanish) | `src/i18n/es.js` | 614 | 100% |
11
13
 
12
- Both locales are fully translated as of this writing. That won't stay automatically true — every time a new rule (or a new i18n key) is added to `en.js`, `fr.js` needs the same key added, or it silently falls back to English for that string (see the fallback behavior below). There's no automated check for this yet; diff `Object.keys(require('./src/i18n/en.js'))` against `fr.js` after adding a rule to catch drift before it ships.
14
+ All four locales are fully translated as of this writing. That won't stay automatically true — every time a new rule (or a new i18n key) is added to `en.js`, every other locale needs the same key added, or it silently falls back to English for that string (see the fallback behavior below). `tests/i18n/i18n-locale-completeness.test.js` catches this: it fails the build if a locale listed in `FULLY_TRANSLATED_LOCALES` (currently `fr`, `de`, `es`) is missing any key present in `en.js`, and fails for *any* locale that has an orphaned key not in `en.js` (a sign of a typo or a stale key left behind after a rule was removed). Run `npm run i18n:report` any time to see per-locale coverage.
13
15
 
14
16
  ## Selecting a locale
15
17
 
@@ -17,7 +19,7 @@ Both locales are fully translated as of this writing. That won't stay automatica
17
19
  runDomRulesInPage(url, null, { locale: 'fr' }, null);
18
20
  ```
19
21
 
20
- Default is `'en'` if omitted. Any string is accepted — an unrecognized locale (e.g. `'de'`, not yet built) behaves exactly like a locale that's 0% translated: every string falls through to English (see below), not an error.
22
+ Default is `'en'` if omitted. Any string is accepted — an unrecognized locale (e.g. `'ja'`, not yet built) behaves exactly like a locale that's 0% translated: every string falls through to English (see below), not an error.
21
23
 
22
24
  ## Fallback behavior (per-string, not per-locale)
23
25
 
@@ -40,8 +42,9 @@ Both are included in the result alongside the already-resolved text, so you can
40
42
 
41
43
  ## Contributing a translation
42
44
 
43
- 1. Open `src/i18n/en.js` — it's the canonical key list (590 entries, one `module.exports` object of `key: string`).
44
- 2. Add matching keys to `src/i18n/<locale>.js` (create the file if the locale doesn't exist yet — follow `fr.js`'s structure exactly: `'use strict'; module.exports = { ...keys };`).
45
- 3. You don't need every key on day one — the fallback behavior above means a partial translation degrades gracefully to English per-string, exactly like `fr` does today. Ship what you have.
46
- 4. Keep `{{placeholder}}` tokens in translated strings exactly as they appear in the English source — they're substituted verbatim regardless of locale (e.g. `{{element}}`, `{{role}}`).
47
- 5. Run `npm run build && npm test` — there's no locale-completeness test today (a partial locale is valid, not a failure), but this confirms nothing else broke.
45
+ 1. Scaffold the file: `npm run i18n:new <locale>` (e.g. `npm run i18n:new de`) creates `src/i18n/<locale>.js` with all of `en.js`'s keys already present, each seeded with the English text as a placeholder. It refuses to overwrite an existing locale file unless you pass `--force`.
46
+ 2. Replace the placeholder values with real translations, key by key. Leave any you're unsure about as-is for now — a value identical to English is treated as untranslated, not broken (see the fallback behavior above).
47
+ 3. Check progress any time with `npm run i18n:report` — it prints, per locale, how many keys have been translated vs. still match the English placeholder, plus any missing or orphaned keys.
48
+ 4. You don't need every key on day one — the fallback behavior above means a partial translation degrades gracefully to English per-string, exactly like `fr`/`de`/`es` did during their own early stages. Ship what you have. A partial locale is valid and won't fail `i18n-locale-completeness.test.js` unless you also add it to `FULLY_TRANSLATED_LOCALES` in that file — only do that once `npm run i18n:report` shows 100% coverage.
49
+ 5. Keep `{{placeholder}}` tokens (and `{{#foo}}...{{/foo}}` conditional blocks) in translated strings exactly as they appear in the English source — they're substituted/evaluated verbatim regardless of locale (e.g. `{{element}}`, `{{role}}`). Never translate HTML tag/attribute names (`<img>`, `aria-label`, `role="dialog"`, etc.) — they're code identifiers, not prose.
50
+ 6. Run `npm run build && npm test` to confirm nothing broke, including locale-completeness.
@@ -91,6 +91,24 @@ const result = await page.evaluate(wrapperFn, {
91
91
 
92
92
  This is the only pattern that gives every rule real computed layout, so it's the one to reach for if you need `target-size-minimum` or any other geometry-dependent check to actually run instead of reporting `notApplicable`.
93
93
 
94
+ ## Pattern 3 — a standalone `<script>` tag (no driver, no bundler)
95
+
96
+ Good for: a manual check against a page open in a real browser, a bookmarklet, or any other context where there's no automation driver and no build step to reach for.
97
+
98
+ `@surea11y/core` ships `surea11y.browser.js` at the package root, a bundle generated from the same rule sources as `src/core.js`. Loading it directly defines one global, `a11ycore`:
99
+
100
+ ```html
101
+ <script src="node_modules/@surea11y/core/surea11y.browser.js"></script>
102
+ <script>
103
+ const result = a11ycore.runa11yCoreInPage(location.href, null, {}, null);
104
+ console.log(result.checksResults.filter((r) => r.outcome === 'fail'));
105
+ </script>
106
+ ```
107
+
108
+ `a11ycore.runa11yCoreInPage` is the exact same function described in Pattern 2 above — the bundle exists only to solve *loading* it without `require`/a module system, not to add a separate API surface. It carries the same self-containment property (own inlined rule catalog, no free variables), which is what makes a plain `<script>` tag sufficient.
109
+
110
+ Deliberately excluded from this bundle: `runa11yCoreAcrossFrames`/`a11yCoreEnableFrameResponder`. Cross-frame scanning needs the embedded frame to load the engine and opt in too (see "Cross-frame scanning" below) — not a fit for a single dropped-in script tag. Use the npm package directly if you need it.
111
+
94
112
  ## Scoping a scan to part of the page
95
113
 
96
114
  Pass a CSS selector as the 2nd argument (`contextSelector`) to scan one subtree instead of the whole document — e.g. `runDomRulesInPage(url, '#app', {}, null)` to skip a surrounding CMS chrome you don't control. Pass an array of selectors (or a single comma-separated selector string) to scan multiple, possibly disjoint regions in one run — e.g. `runDomRulesInPage(url, ['#header', '#main'], {}, null)`. See [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md) for the full `contextSelector` reference and for `excludeSelectors`, the complementary "skip specific elements anywhere" option.
@@ -112,7 +130,7 @@ if (failures.length > 0) {
112
130
 
113
131
  Notes for CI specifically:
114
132
  - `cantTell` outcomes are advisory by design (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md#outcome-values)) — most teams log them without failing the build, since they require human judgment the CI run can't make.
115
- - For "only fail on *new* violations," the CLI has a built-in baseline/allowlist mechanism (`--write-baseline`/`--baseline`, see [`BASELINE.md`](./BASELINE.md)). Calling the library directly, the same matching logic is available as `buildBaselineEntries(result)`/`matchBaseline(result, baselineEntries)` from `require('@surea11y/core/src/baseline')` — or diff `checksResults` against a saved prior run yourself if your pages don't fit that model (see `BASELINE.md`'s "known limitation").
133
+ - For "only fail on *new* violations," the CLI has a built-in baseline/allowlist mechanism (`--write-baseline`/`--baseline`, see [`BASELINE.md`](./BASELINE.md)). Calling the library directly, the same matching logic is available as `buildBaselineEntries(result)`/`matchBaseline(result, baselineEntries)` from `require('@surea11y/core/baseline')` — or diff `checksResults` against a saved prior run yourself if your pages don't fit that model (see `BASELINE.md`'s "known limitation").
116
134
  - Prefer Pattern 1 (jsdom) in CI unless you specifically need real-browser layout — it avoids the extra weight of a Puppeteer/Playwright + browser-binary install in your pipeline.
117
135
 
118
136
  ## Browser extension context