@surea11y/webdriverio 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,13 +28,13 @@ await browser.url('https://example.com/');
28
28
  const results = await new A11yCoreBuilder({ browser })
29
29
  .include('#main') // optional -- call multiple times for multi-region scans
30
30
  .exclude('.cookie-banner') // optional
31
- .withTags(['wcag2a', 'wcag2aa'])
31
+ .withWcag('2.2', 'AA') // the rules for WCAG 2.2's A and AA criteria
32
32
  .disableRules(['meta-refresh-no-exceptions'])
33
33
  .options({ contrast: { mode: 'auditorAssist' } })
34
34
  .analyze();
35
35
 
36
36
  console.log(results.checksResults.filter(r => r.outcome === 'fail'));
37
- console.log(results.engine.version); // the @surea11y/core release that scanned, e.g. "1.10.0"
37
+ console.log(results.engine.version); // the @surea11y/core release that scanned, e.g. "1.11.1"
38
38
  await browser.deleteSession();
39
39
  ```
40
40
 
@@ -50,7 +50,9 @@ WebdriverIO v9 defaults to the WebDriver **BiDi** protocol. A long-lived session
50
50
 
51
51
  ### Composing the rule filters
52
52
 
53
- `withTags()`/`disableRules()` above have counterparts: `.withRules([...])` (only run these specific rule IDs) and `.disableTags([...])` (never run rules carrying any of these tags). All four compose the same way similar allow/deny-list options do in other accessibility testing tools, with one non-obvious rule worth knowing: a "disable" always wins over a "with" on the same ID/tag (e.g. `.withRules(['a']).disableRules(['a'])` drops `'a'` entirely), and combining `.withRules()` **and** `.withTags()` together requires a rule to satisfy *both* (`@surea11y/core`'s default `includeMode: 'and'`), not either one.
53
+ `.withWcag(version, level)` selects by conformance target: `.withWcag('2.2', 'AA')` runs the rules for WCAG 2.2's Level A and AA criteria, by the level each criterion has in that version (4.1.1 Parsing, which 2.2 removed, is part of a `'2.1'` target and not of a `'2.2'` one). `.withBestPractices()` adds the rules that name no WCAG criterion, such as `region` and `heading-order`; on its own it runs only those. `.withRules()` and `.withTags()` add to a target's rules, and the disable methods apply after. A version other than `'2.0'`, `'2.1'` or `'2.2'`, or a level other than `'A'`, `'AA'` or `'AAA'`, throws a `TypeError` with `code: 'INVALID_RUN_ONLY'` at the call. Both need `@surea11y/core` 1.11 or later.
54
+
55
+ `withTags()`/`disableRules()` have counterparts: `.withRules([...])` (only run these specific rule IDs) and `.disableTags([...])` (never run rules carrying any of these tags). All four compose the same way similar allow/deny-list options do in other accessibility testing tools, with one non-obvious rule worth knowing: a "disable" always wins over a "with" on the same ID/tag (e.g. `.withRules(['a']).disableRules(['a'])` drops `'a'` entirely), and combining `.withRules()` **and** `.withTags()` together requires a rule to satisfy *both* (`@surea11y/core`'s default `includeMode: 'and'`), not either one.
54
56
 
55
57
  A rule or tag list that names nothing `@surea11y/core` knows makes `analyze()` reject with `code: 'INVALID_RUN_ONLY'` (see [When a scan can't run as asked](#when-a-scan-cant-run-as-asked)), so a typo such as `.withTags(['wcag2.2aa'])` can't run no rule at all and pass. An unknown name beside known ones is ignored, with a warning in the page's console. The four methods also throw a `TypeError` with the same `code` straight away for anything but a string or an array of strings, such as `undefined` from a missing config value.
56
58
 
@@ -58,7 +60,7 @@ Rule IDs change rarely, but core 1.10.0 changed two: `label-title-only` is depre
58
60
 
59
61
  `.exclude(selector)` above excludes globally. Pass a second argument to scope it to specific rule IDs instead: `.exclude('.mat-select', { rules: ['aria-required-children'] })` skips `.mat-select` for that rule only — every other rule still sees it. Global and rule-scoped `.exclude()` calls compose freely.
60
62
 
61
- **Create one builder per scan.** `A11yCoreBuilder` is a mutable object with no reset between `.analyze()` calls — `include()`/`exclude()`/`withRules()`/`disableRules()`/`withTags()`/`disableTags()`/`options()`/`withCustomRules()` all push onto or merge into internal state that persists for the instance's lifetime. Calling one of them again before a second `.analyze()` call *accumulates* on top of the first scan's scope rather than replacing it (this is exactly what makes "call `.include()` several times for one scan," above, work — the same accumulation just also applies across separate scans if you reuse an instance). `.reportOnly()`/`.frames()`/`.elementRef()` are the exception: each call replaces the previous value instead of merging with it.
63
+ **Create one builder per scan.** `A11yCoreBuilder` is a mutable object with no reset between `.analyze()` calls — `include()`/`exclude()`/`withRules()`/`disableRules()`/`withTags()`/`disableTags()`/`options()`/`withCustomRules()`/`withPacks()` all push onto or merge into internal state that persists for the instance's lifetime. Calling one of them again before a second `.analyze()` call *accumulates* on top of the first scan's scope rather than replacing it (this is exactly what makes "call `.include()` several times for one scan," above, work — the same accumulation just also applies across separate scans if you reuse an instance). `.reportOnly()`/`.frames()`/`.elementRef()`/`.withWcag()`/`.withBestPractices()` are the exception: each call replaces the previous value instead of merging with it.
62
64
 
63
65
  This binding works against any browser WebdriverIO can drive; the tests and examples use headless Chrome via `chromedriver`, which WebdriverIO manages automatically.
64
66
 
@@ -77,14 +79,14 @@ describe('accessibility gate', () => {
77
79
  const results = await new A11yCoreBuilder({ browser }).reportOnly(['fail']).analyze();
78
80
 
79
81
  assert.strictEqual(results.checksResults.length, 0, formatFailures(results));
80
- // An include() scope that matched nothing, or a custom rule that did not
81
- // run, leaves no failure behind: check for them too.
82
+ // An include() scope that matched nothing, or a custom rule or a pack
83
+ // that did not run, leaves no failure behind: check for them too.
82
84
  assert.deepStrictEqual(getScanGaps(results), [], formatFailures(results));
83
85
  });
84
86
  });
85
87
  ```
86
88
 
87
- See `examples/e2e-test-example.test.js` for a fuller, runnable version (`npm run example:e2e`), driven by `examples/wdio.conf.js` — one test proving real violations get caught (unlabeled button, missing `alt`), one proving a well-formed page passes cleanly. It uses the **WebdriverIO testrunner** (`@wdio/cli` + mocha), which is how real WebdriverIO suites are written. This project's own *unit* suite (`tests/*.test.js`) uses `node:test` + `remote()`, the right tool for exercising the builder API in isolation.
89
+ See `examples/e2e-test-example.test.js` for a fuller, runnable version (`npm run example:e2e`), driven by `examples/wdio.conf.js` — one test proving real violations get caught (unlabeled button, missing `alt`), one proving a well-formed page passes cleanly, and one gating a page on a pack's checklist (see [Scanning with packs](#scanning-with-packs-standards-checklists-and-your-own-rules)). It uses the **WebdriverIO testrunner** (`@wdio/cli` + mocha), which is how real WebdriverIO suites are written. This project's own *unit* suite (`tests/*.test.js`) uses `node:test` + `remote()`, the right tool for exercising the builder API in isolation.
88
90
 
89
91
  Note the gate above uses `node:assert`, not WebdriverIO's bundled `expect`: WebdriverIO's `expect` takes **at most one argument** — there's no `expect(value, message)` custom-failure-message overload the way Playwright's `expect` has — so `node:assert`'s message parameter is what carries `formatFailures()`' readable block onto a failed assertion.
90
92
 
@@ -95,12 +97,12 @@ A bare length/equality assertion alone gets you a *working* gate, but the failur
95
97
  ```
96
98
  1) button-name-present (serious): This button has no accessible name.
97
99
  at html > body > button
98
- Provide visible button text or a programmatic accessible-name mechanism (for example aria-label) so assistive technologies can identify the button.
99
- 2) img-alt-present (serious): Missing alt attribute on <img>.
100
+ Give the button visible text, or name it with aria-label, so screen reader users know what it does.
101
+ 2) img-alt-present (serious): This <img> has no alt attribute.
100
102
  at #host >>> p > img
101
- Add an alt attribute (use alt="" only for decorative images).
103
+ Add an alt attribute that says what the image shows, or alt="" if the image is purely decorative.
102
104
 
103
- Scanned with @surea11y/core 1.10.0.
105
+ Scanned with @surea11y/core 1.11.1.
104
106
  ```
105
107
 
106
108
  An element inside a shadow tree is located as `host >>> selector` (the second entry above); `formatOccurrenceLocation(occurrence)` gives the same text if you print occurrences yourself. `formatFailures(results.checksResults)` still works and prints only the entries. A `.frames(true)` result is not one result, so `formatFailures()` throws a `TypeError` for it: format `results.topFrame` and each entry of `results.frames` on its own.
@@ -122,7 +124,17 @@ for (const frame of results.frames) {
122
124
 
123
125
  The mechanics here are genuinely different from those bindings, though, because WebdriverIO's frame model is different. Puppeteer/Playwright expose `page.frames()` — an array of independent `Frame` objects, each with its own `.evaluate()`/`.$()` — so a binding can iterate them freely. WebdriverIO instead has a single **stateful current context**: `browser.switchFrame(iframeElement)` changes which document `browser.execute()`/`browser.$()` run against, and `browser.switchFrame(null)` returns to the top-level frame. There is no array of frame objects. So `.frames(true)` enumerates each context's direct-child `<iframe>` elements with `browser.$$('iframe')`, switches into each, scans the now-current document, and recurses into that frame's own children (re-navigating from the top each time, since element references and BiDi context ids don't reliably survive a context switch). Cross-origin iframes are reached with no extra work — WebdriverIO's automation layer switches into them the same as same-origin ones, exactly like Puppeteer/Playwright's CDP does.
124
126
 
125
- **`.include()` scopes the top frame only.** Each sub-frame is scanned whole, as `@surea11y/core`'s own `runa11yCoreAcrossFrames` does: the selectors belong to the top document, and since core 1.10.0 a selector that matches nothing scans nothing, so passing them down would leave every frame without them unscanned. `.exclude()` and the rule filters apply in every frame. An `INVALID_RUN_ONLY` or `INVALID_CONTEXT_SELECTOR` error rejects `analyze()` from the top frame, before any sub-frame is scanned; a sub-frame that fails for another reason is listed in `results.frames` as `{ url, error }`.
127
+ **`.include()` scopes the top frame only.** Each sub-frame is scanned whole, as `@surea11y/core`'s own `runa11yCoreAcrossFrames` does: the selectors belong to the top document, and since core 1.10.0 a selector that matches nothing scans nothing, so passing them down would leave every frame without them unscanned. `.exclude()` and the rule filters apply in every frame. An `EngineError` (see [When a scan can't run as asked](#when-a-scan-cant-run-as-asked)) rejects `analyze()` from the top frame, before any sub-frame is scanned; a sub-frame that fails for another reason is listed in `results.frames` as `{ url, error }`.
128
+
129
+ `@surea11y/core`'s reporters take this result as it is (core 1.11.1 and later): `renderSarifReport(results)`, `renderJunitReport(results)`, `renderHtmlReport(results)` and the baseline functions report every frame's findings, and a frame that couldn't be scanned as not scanned, so it never reads as a pass.
130
+
131
+ ```js
132
+ const fs = require('node:fs');
133
+ const { renderSarifReport } = require('@surea11y/core/sarif');
134
+
135
+ const results = await new A11yCoreBuilder({ browser }).frames(true).analyze();
136
+ fs.writeFileSync('a11y.sarif', renderSarifReport(results));
137
+ ```
126
138
 
127
139
  Verified against a real cross-origin page (`https://example.org/` embedded in an unrelated origin) and against a real nested-iframe page (top → child → grandchild) — see `tests/builder.test.js`. Default off, so plain `.analyze()` is unaffected unless you opt in. Unlike script-injection-based accessibility tools (which need a `postMessage`-based protocol to reach cross-origin iframes, since they're injected as a plain `<script>` subject to the same-origin policy), this needs no extra `@surea11y/core` engine support at all.
128
140
 
@@ -140,6 +152,8 @@ console.log(results.checksResults); // only fail/cantTell entries, pass/notAppli
140
152
 
141
153
  Valid outcome values are `'pass'`, `'fail'`, `'cantTell'`, `'notApplicable'`. This is pure binding-layer filtering — the engine itself still computes every rule; nothing about the scan itself changes. Combines with `.frames(true)`: the filter is applied to `results.topFrame` and each entry of `results.frames` independently.
142
154
 
155
+ A result trimmed this way is no longer the whole scan to `@surea11y/core`'s reporters: JUnit counts one test per rule it finds, and EARL makes one assertion per rule. To keep a result small and still hand it to them, ask the engine for a compact one instead, with `.options({ output: { detail: 'findings' } })`: a rule that passed or did not apply keeps only its outcome, and every reporter reads it as the full result. An empty page's result goes from about 238 KB to 82 KB.
156
+
143
157
  ### Getting a live element, not just a selector string
144
158
 
145
159
  By default each occurrence carries a CSS selector + HTML snippet, not a live reference to the element. Opt in to a real `WebdriverIO.Element` with `.elementRef(true)`:
@@ -201,6 +215,8 @@ Invalid input (a missing/empty `id`, or a `runInPage`/`applicability` that's nei
201
215
 
202
216
  - `INVALID_RUN_ONLY`: none of the rule IDs, or none of the tags, given to `.withRules()`/`.withTags()` is one the engine knows.
203
217
  - `INVALID_CONTEXT_SELECTOR`: an `.include()` selector is not valid CSS; `error.selector` names it.
218
+ - `INVALID_ENGINE_OPTIONS`: with `.options({ strictOptions: true })`, an option the engine doesn't read or a value it can't use. The message names each, with the closest known option (`lcoale`: did you mean `locale`?). Without `strictOptions`, a likely typo is only warned about in the page's console.
219
+ - `PAGE_BUILTINS_BROKEN`: the page's own scripts broke a JavaScript built-in the engine relies on (`Array.prototype.filter`, `Map`, `JSON.stringify`, ...), so no result it gave could be trusted. The message names each one.
204
220
 
205
221
  ```js
206
222
  const { A11yCoreBuilder, EngineError } = require('@surea11y/webdriverio');
@@ -219,8 +235,36 @@ Two other cases don't throw but leave part of the page unchecked, so a result th
219
235
 
220
236
  - **An `.include()` selector that matches nothing.** Since core 1.10.0 nothing is then scanned (it used to fall back to the whole page): every rule reports `notApplicable`, and `results.contextMatch` is `{ elementCount: 0, unmatchedSelectors: [...] }`. With several selectors, one that matches nothing is named there while the others are scanned.
221
237
  - **A custom rule that did not run**, listed with its reason in `results.skippedCustomRules`.
238
+ - **A pack that did not run**, listed with its reason in `results.skippedPacks` (see [Scanning with packs](#scanning-with-packs-standards-checklists-and-your-own-rules)).
239
+
240
+ `analyze()` prints each of these with `console.warn`, and `getScanGaps(results)` returns them as `[{ kind, message, ... }]` (`kind` is `context-not-found`, `context-partly-not-found`, `custom-rule-skipped` or `pack-skipped`), so a gate can fail on them, as the E2E example above does. `formatFailures(results)` lists them too.
241
+
242
+ ### Scanning with packs: standards, checklists and your own rules
222
243
 
223
- `analyze()` prints each of these with `console.warn`, and `getScanGaps(results)` returns them as `[{ kind, message, ... }]` (`kind` is `context-not-found`, `context-partly-not-found` or `custom-rule-skipped`), so a gate can fail on them, as the E2E example above does. `formatFailures(results)` lists them too.
244
+ A pack brings rules, variants of core's rules, a standard or a checklist, and their profiles and messages, from a package of its own: a national standard such as RGAA, or your organisation's accessibility policy. `@surea11y/core` runs a pack as it runs its own rules and standards, so its results, rollups and report entries look like core's. Packs need `@surea11y/core` 1.11.1 or later.
245
+
246
+ `.withPacks()` registers packs in the page with `browser.execute()`, and in every frame with `.frames(true)`, before the scan; a pack's profile is chosen with `.options({ profile })`:
247
+
248
+ ```js
249
+ const acmePolicy = require('@acme/a11y-policy'); // a pack
250
+
251
+ const results = await new A11yCoreBuilder({ browser })
252
+ .withPacks(acmePolicy)
253
+ .options({ profile: 'acme-web-2026' })
254
+ .analyze();
255
+
256
+ results.engine.packs; // ['@acme/a11y-policy@1.0.0']
257
+ results.engine.profile; // 'acme-web-2026'
258
+
259
+ // A checklist's items, each one result, beside core's WCAG rollups:
260
+ results.rulesResults.filter((r) => r.meta && r.meta.standard === 'Acme web policy');
261
+ ```
262
+
263
+ A pack's own rules appear in `checksResults` like core's, with ids that start with its namespace (`acme-no-autoplay`). Without its profile, a pack's rules run only when selected, with `.withRules()` or by a tag. `@surea11y/core`'s HTML, SARIF and JUnit reports show a standard's or a checklist's items in a section of their own. Pass an array, or call `.withPacks()` again, to scan with several packs.
264
+
265
+ **Writing a pack.** A pack is an object made with `definePack` from `@surea11y/core/pack`. [`examples/acme-policy-pack.js`](./examples/acme-policy-pack.js) is a small one: a house rule, a checklist profile (WCAG 2.2 A and AA, `region`, and the house rule) and two checklist items. `npm run example:pack` scans a page with it, and [`examples/e2e-test-example.test.js`](./examples/e2e-test-example.test.js) uses it as a test gate. For a pack of your own, `npx surea11y-pack new <folder>` (from `@surea11y/core`) writes a package that works as generated, with its tests, docs and lint; core's [`PACKS.md`](https://github.com/SureA11y/core/blob/main/docs/PACKS.md) is the guide.
266
+
267
+ **When a pack doesn't run.** `.withPacks()` builds the page's script with the `@surea11y/core` this binding loads, and checks each pack as it does: `.analyze()` throws before scanning for a pack that core can't run, such as one whose `core` range leaves out the installed core, or whose profile names a rule that doesn't exist. A pack the page then doesn't run (one named with a raw `.options({ packs })` and never registered, or a page running another core version, when two copies of core are installed) is listed in `results.skippedPacks` with the reason. `getScanGaps()` reports that as `'pack-skipped'`, and `.analyze()` warns about it, so a test gate that asserts on `getScanGaps(results)` fails rather than passing with the pack's checks never run. Install the pack beside the binding, so npm resolves one core for both.
224
268
 
225
269
  ### Element addressing beyond a CSS selector
226
270
 
@@ -230,11 +274,21 @@ An element inside a shadow tree is different: its `selector` holds only inside i
230
274
 
231
275
  ## TypeScript
232
276
 
233
- `src/A11yCoreBuilder.d.ts` (re-exported from `src/index.d.ts`, wired up via `package.json`'s `types` field) ships types for the whole builder API plus `@surea11y/core`'s native result shapes (`A11yCoreResult`, `CheckResult`, `Occurrence`, `CompositeResult`, etc.). The result shapes are built on the types `@surea11y/core` ships itself, so fields such as `engine.version`, `engine.environment`, `contextMatch`, `skippedCustomRules`, `margin` and `shadowHostSelectors` follow the engine; this package adds `occurrence.element` and the `.frames(true)` shape. `EngineError`, `EngineErrorCode` and `ScanGap` come from `@surea11y/binding-base`. `analyze()` is typed `Promise<A11yCoreResult | A11yCoreMultiFrameResult>` — narrow on `'topFrame' in results` (or cast, if you already know which mode you called) to get the specific shape back, since a fluent builder can't statically track that `.frames(true)` was called earlier in the chain. `occurrence.element` is typed `WebdriverIO.Element | null`. `webdriverio` is a `peerDependencies` entry (not just `devDependencies`) since the class's `browser` argument and `Occurrence#element` both come from its global `WebdriverIO` type namespace — consumers need their own `webdriverio` install for the types to resolve, same as they already do to construct a `browser` in the first place.
277
+ `src/A11yCoreBuilder.d.ts` (re-exported from `src/index.d.ts`, wired up via `package.json`'s `types` field) ships types for the whole builder API plus `@surea11y/core`'s native result shapes (`A11yCoreResult`, `CheckResult`, `Occurrence`, `CompositeResult`, etc.). The result shapes are built on the types `@surea11y/core` ships itself, so fields such as `engine.version`, `engine.environment`, `contextMatch`, `skippedCustomRules`, `margin` and `shadowHostSelectors` follow the engine; this package adds `occurrence.element` and the `.frames(true)` shape. `EngineError`, `EngineErrorCode` and `ScanGap` come from `@surea11y/binding-base`, and so do the types of `withWcag()` (`WcagVersion`, `WcagLevel`) and `withPacks()`, which takes a pack made with `definePack` (`PackLike`). Code that handles every `ScanGap` kind it knows and treats the rest in an `else` branch needs a branch for `'pack-skipped'`, which carries `pack` rather than `selectors`. `analyze()` is typed `Promise<A11yCoreResult | A11yCoreMultiFrameResult>` — narrow on `'topFrame' in results` (or cast, if you already know which mode you called) to get the specific shape back, since a fluent builder can't statically track that `.frames(true)` was called earlier in the chain. `occurrence.element` is typed `WebdriverIO.Element | null`. `webdriverio` is a `peerDependencies` entry (not just `devDependencies`) since the class's `browser` argument and `Occurrence#element` both come from its global `WebdriverIO` type namespace — consumers need their own `webdriverio` install for the types to resolve, same as they already do to construct a `browser` in the first place.
278
+
279
+ ## What changes with `@surea11y/core` 1.11
280
+
281
+ The binding passes rule IDs, tags and options through to the engine, so these show in results without a change here (see core's [`CHANGELOG.md`](https://github.com/SureA11y/core/blob/main/CHANGELOG.md) for the full list):
282
+
283
+ - **Messages are reworded** in all five languages to say plainly what is wrong and what to do (`This <img> has no alt attribute.`). Message keys, rule IDs, reason codes and baselines are unchanged; a test that matches message text needs the new wording.
284
+ - **Contrast and target size measure what is on screen.** In a browser, the contrast rules check only text a reader can see, measure it against what is painted under it, and measure form field values and placeholders. `target-size-minimum` measures the area a pointer can hit, and exempts only links in a sentence. Expect new findings, and some old ones gone.
285
+ - **`aria-hidden-focus` maps to 4.1.2 only**, no longer to 2.4.7 Focus Visible, so `.withTags('wcag2aa')` alone no longer selects it.
286
+ - **A custom rule counts toward its WCAG criterion.** A `.withCustomRules()` rule with `meta.wcagSc` (or a WCAG `normativeMappings` entry) fails that criterion's rollup when it fails, and custom rules run after core's.
287
+ - **On/off options read as meant**: `.options({ perfStats: 'false' })` is off, where it was on. `.options({ strictOptions: true })` rejects any option the engine doesn't read (see [When a scan can't run as asked](#when-a-scan-cant-run-as-asked)).
234
288
 
235
289
  ## Relationship to `@surea11y/playwright` and `@surea11y/puppeteer`
236
290
 
237
- This binding's builder API is deliberately the same shape as [`@surea11y/playwright`](https://github.com/SureA11y/playwright)'s and `@surea11y/puppeteer`'s — same method names, same mutability contract, same result shapes — so the accessibility-gate logic reads almost identically across all three. The state-accumulation half of the builder (`include`/`exclude`/`withTags`/`disableTags`/`withRules`/`disableRules`/`options`/`withCustomRules`/`reportOnly`) is not copied across bindings — `A11yCoreBuilder` here extends `A11yCoreBuilderBase` from [`@surea11y/binding-base`](https://github.com/SureA11y/binding-base), the single shared implementation every sibling binding depends on.
291
+ This binding's builder API is deliberately the same shape as [`@surea11y/playwright`](https://github.com/SureA11y/playwright)'s and `@surea11y/puppeteer`'s — same method names, same mutability contract, same result shapes — so the accessibility-gate logic reads almost identically across all three. The state-accumulation half of the builder (`include`/`exclude`/`withTags`/`disableTags`/`withRules`/`disableRules`/`withWcag`/`withBestPractices`/`options`/`withCustomRules`/`withPacks`/`reportOnly`) is not copied across bindings — `A11yCoreBuilder` here extends `A11yCoreBuilderBase` from [`@surea11y/binding-base`](https://github.com/SureA11y/binding-base), the single shared implementation every sibling binding depends on.
238
292
 
239
293
  Where it genuinely diverges is WebdriverIO's driver model, and those differences are real, not cosmetic:
240
294
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@surea11y/webdriverio",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "WebdriverIO binding for surea11y -- scan real, live-rendered pages for accessibility issues.",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -40,23 +40,24 @@
40
40
  "LICENSE"
41
41
  ],
42
42
  "dependencies": {
43
- "@surea11y/binding-base": "^1.2.0",
44
- "@surea11y/core": "^1.10.0"
43
+ "@surea11y/binding-base": "^1.3.1",
44
+ "@surea11y/core": "^1.11.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "webdriverio": ">=8.0.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@wdio/cli": "^9.30.0",
51
- "@wdio/local-runner": "^9.30.0",
50
+ "@wdio/cli": "^9.32.0",
51
+ "@wdio/local-runner": "^9.32.0",
52
52
  "@wdio/mocha-framework": "^9.32.0",
53
- "@wdio/spec-reporter": "^9.29.1",
54
- "typescript": "^5.9.3",
55
- "webdriverio": "^9.0.0"
53
+ "@wdio/spec-reporter": "^9.32.0",
54
+ "typescript": "^7.0.2",
55
+ "webdriverio": "^9.32.0"
56
56
  },
57
57
  "scripts": {
58
58
  "test": "node --test --test-concurrency=1 tests/*.test.js",
59
59
  "example": "node examples/basic-scan.js",
60
- "example:e2e": "wdio run examples/wdio.conf.js"
60
+ "example:e2e": "wdio run examples/wdio.conf.js",
61
+ "example:pack": "node examples/scan-with-pack.js"
61
62
  }
62
63
  }
@@ -10,6 +10,8 @@ import type {} from 'webdriverio';
10
10
  // { topFrame, frames } shape of .frames(true). The names below are the ones
11
11
  // this file has always exported.
12
12
  import type * as Core from '@surea11y/core';
13
+ import type { PackLike, WcagVersion, WcagLevel } from '@surea11y/binding-base';
14
+ export type { PackLike, WcagVersion, WcagLevel } from '@surea11y/binding-base';
13
15
 
14
16
  export type Outcome = Core.Outcome;
15
17
  export type OutcomeNormalized = Core.OutcomeNormalized;
@@ -137,10 +139,23 @@ export class A11yCoreBuilder {
137
139
  withRules(ruleIds: string | string[]): this;
138
140
  /** Never run these specific rule IDs (applied after withRules). */
139
141
  disableRules(ruleIds: string | string[]): this;
142
+ /**
143
+ * Run the rules for a WCAG conformance target: `withWcag('2.2', 'AA')`
144
+ * runs those of WCAG 2.2's A and AA criteria. withTags()/withRules() add
145
+ * to them. A later call replaces the target.
146
+ */
147
+ withWcag(version: WcagVersion | 2.0 | 2.1 | 2.2, level?: WcagLevel): this;
148
+ /** Also run the best-practice rules, those that name no WCAG criterion. */
149
+ withBestPractices(enabled?: boolean): this;
140
150
  /** Merge arbitrary engineOptions (locale, contrast.mode, policyContract, ...). */
141
151
  options(partialEngineOptions: Record<string, unknown>): this;
142
152
  /** Register one or more custom rules for just this scan. Call multiple times to accumulate. */
143
153
  withCustomRules(rules: CustomRuleDescriptor | CustomRuleDescriptor[]): this;
154
+ /**
155
+ * Packs from `@surea11y/core/pack` (core 1.11 or later): registered in each
156
+ * frame before the scan, and named in `engineOptions.packs`.
157
+ */
158
+ withPacks(packs: PackLike | PackLike[]): this;
144
159
  /** Post-filter checksResults down to only the given outcomes. */
145
160
  reportOnly(outcomes: Outcome | Outcome[]): this;
146
161
  /** Opt in to also scanning every sub-frame on the page (including cross-origin and nested iframes). */
@@ -153,8 +168,10 @@ export class A11yCoreBuilder {
153
168
  * when .frames(true) was used. Rejects with an `EngineError` (from
154
169
  * `@surea11y/binding-base`, re-exported here) whose `code` is
155
170
  * `INVALID_RUN_ONLY` when a rule or tag list names nothing @surea11y/core
156
- * knows, or `INVALID_CONTEXT_SELECTOR` (with `selector`) when an include()
157
- * selector does not parse.
171
+ * knows, `INVALID_CONTEXT_SELECTOR` (with `selector`) when an include()
172
+ * selector does not parse, `INVALID_ENGINE_OPTIONS` for an option
173
+ * `strictOptions: true` rejects, or `PAGE_BUILTINS_BROKEN` when the page's
174
+ * scripts broke a JavaScript built-in the engine relies on.
158
175
  */
159
176
  analyze(): Promise<A11yCoreResult | A11yCoreMultiFrameResult>;
160
177
  }
@@ -163,7 +180,7 @@ export class A11yCoreBuilder {
163
180
  * Formats a result, or its checksResults array, into a short, human-readable
164
181
  * block -- one entry per fail/cantTell occurrence, not per rule. Given the
165
182
  * whole result, it also lists what the scan left out (an include() scope
166
- * that matched nothing, a custom rule that did not run) and the
183
+ * that matched nothing, a custom rule or a pack that did not run) and the
167
184
  * @surea11y/core release that produced it. Meant for an assertion
168
185
  * library's failure-message parameter, e.g.
169
186
  * `assert.strictEqual(fails.length, 0, formatFailures(results))`.
@@ -136,14 +136,14 @@ const inPageScan = createInPageScan(runa11yCoreInPage);
136
136
  *
137
137
  * Create one builder per scan. This is a mutable object with no reset
138
138
  * between analyze() calls: include()/exclude()/withRules()/disableRules()/
139
- * withTags()/disableTags()/options()/withCustomRules() all push onto or
139
+ * withTags()/disableTags()/options()/withCustomRules()/withPacks() all push onto or
140
140
  * merge into internal state that persists for the instance's lifetime, so
141
141
  * calling one of them again before a second analyze() call accumulates on
142
142
  * top of the first scan's scope rather than replacing it (intentional for
143
143
  * "call include() several times for one scan" -- see above -- but a footgun
144
144
  * if you hold one instance across multiple assertions).
145
- * reportOnly()/frames()/elementRef() are the exception: each call replaces
146
- * the previous value rather than merging with it.
145
+ * reportOnly()/frames()/elementRef()/withWcag()/withBestPractices() are the
146
+ * exception: each call replaces the previous value rather than merging with it.
147
147
  */
148
148
  class A11yCoreBuilder extends A11yCoreBuilderBase {
149
149
  /**
@@ -192,8 +192,13 @@ class A11yCoreBuilder extends A11yCoreBuilderBase {
192
192
  // inPageScan takes the same four arguments as runa11yCoreInPage; an
193
193
  // engine error comes back as a plain object, which rethrowEngineError()
194
194
  // throws again here as an EngineError with its `code`.
195
+ // The script that registers withPacks()'s packs in a frame, which the
196
+ // scan names in engineOptions.packs; null without packs. execute() runs
197
+ // a string as a function body, so the script runs as written.
198
+ const packScript = this._packScript();
195
199
  const scanCurrent = async (scope) => {
196
200
  const frameUrl = this._url || (await this._browser.execute(() => document.location.href));
201
+ if (packScript) await this._browser.execute(packScript);
197
202
  const result = rethrowEngineError(
198
203
  await this._browser.execute(inPageScan, frameUrl, scope, engineOptions, runOnly)
199
204
  );
@@ -296,7 +301,7 @@ class A11yCoreBuilder extends A11yCoreBuilderBase {
296
301
 
297
302
  /**
298
303
  * Prints what the scan left out (getScanGaps(): an include() scope that
299
- * matched nothing, a custom rule that did not run) with console.warn.
304
+ * matched nothing, a custom rule or a pack that did not run) with console.warn.
300
305
  * @surea11y/core warns about these too, but in the page's console, which a
301
306
  * WebdriverIO run does not show; a result whose checksResults alone look
302
307
  * clean would otherwise pass without a word.