@surea11y/webdriverio 1.0.0 → 1.0.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.
package/README.md CHANGED
@@ -1,31 +1,20 @@
1
- # surea11y-webdriverio
1
+ # @surea11y/webdriverio
2
2
 
3
- A WebdriverIO binding for [`surea11y`](../surea11y) — scans a real, already-rendered page for accessibility issues using surea11y's DOM-rules engine.
3
+ A WebdriverIO binding for [`@surea11y/core`](https://github.com/rumoroso/surea11y-core) — scans a real, already-rendered page for accessibility issues using surea11y's DOM-rules engine.
4
4
 
5
- This is a **separate project/package** from `surea11y` itself and from its siblings [`surea11y-playwright`](../surea11y-playwright) and [`surea11y-puppeteer`](../surea11y-puppeteer), kept as its own sibling directory rather than a monorepo subfolder — see `ROADMAP.md` §1 for the reasoning (the same reasoning the two sibling bindings already used).
6
-
7
- ## Install (local development)
8
-
9
- `surea11y` isn't published to npm yet, so this package depends on it via a relative `file:` path (see `package.json`):
10
-
11
- ```json
12
- "dependencies": { "surea11y": "file:../core" }
13
- ```
14
-
15
- That means this project must stay a sibling of `surea11y` (or you update the path) for `npm install` to resolve it.
5
+ ## Install
16
6
 
17
7
  ```bash
18
- npm install
19
- npm test
8
+ npm install @surea11y/webdriverio webdriverio
20
9
  ```
21
10
 
22
- `webdriverio` is a `devDependency` here. WebdriverIO v9 auto-manages a local `chromedriver` for you the first time it launches Chrome (no separate Selenium server, and no `npx playwright install`-style step needed for `npm test` to work) — so a fresh clone's `npm install && npm test` launches a real headless Chrome on its own.
11
+ `webdriverio` v9 auto-manages a local `chromedriver` for you the first time it launches Chrome (no separate Selenium server, and no extra browser-download step needed) — so a fresh `npm install` is enough for `npm test` to launch a real headless Chrome on its own.
23
12
 
24
13
  ## Usage
25
14
 
26
15
  ```js
27
16
  const { remote } = require('webdriverio');
28
- const { A11yCoreBuilder } = require('surea11y-webdriverio');
17
+ const { A11yCoreBuilder } = require('@surea11y/webdriverio');
29
18
 
30
19
  const browser = await remote({
31
20
  capabilities: {
@@ -50,17 +39,17 @@ await browser.deleteSession();
50
39
 
51
40
  The builder takes `{ browser }` — the object WebdriverIO's `remote()` returns (standalone mode), or the global `browser` the WDIO testrunner injects (see the E2E section). It must already be navigated to and settled at the URL to scan; the class does not navigate for you.
52
41
 
53
- `results` is surea11y's own native result shape — see [`../surea11y/docs/OUTPUT_SCHEMA.md`](../surea11y/docs/OUTPUT_SCHEMA.md) — not the `violations`/`passes`/`incomplete`/`inapplicable` shape used by other popular accessibility testing tools. The builder's *method names* are modeled on common conventions in this space for migration familiarity; the richer result schema is kept as-is.
42
+ `results` is `@surea11y/core`'s own native result shape — see its [`OUTPUT_SCHEMA.md`](https://github.com/rumoroso/surea11y-core/blob/main/docs/OUTPUT_SCHEMA.md) — not the `violations`/`passes`/`incomplete`/`inapplicable` shape used by other popular accessibility testing tools. The builder's *method names* are modeled on common conventions in this space for migration familiarity; the richer result schema is kept as-is.
54
43
 
55
44
  Also see `examples/basic-scan.js` for a runnable script (`npm run example -- <url>`).
56
45
 
57
46
  ### A note on the protocol: prefer classic WebDriver for this workload
58
47
 
59
- WebdriverIO v9 defaults to the WebDriver **BiDi** protocol. A long-lived session that issues many `browser.execute()` calls — exactly what an accessibility scan does — degraded badly over BiDi in testing here: per-scan latency climbed from ~2s into the *minutes* and eventually the session started dropping commands (`Couldn't resolve command with id N`). Forcing the **classic** WebDriver protocol with `'wdio:enforceWebDriverClassic': true` in the capabilities gave a steady ~150ms per scan with no degradation, and every operation this binding needs (`execute`, `$`, `$$`, `switchFrame`) is fully supported on it. This project's own tests and examples all set that flag, and it's recommended for yours. The binding itself works under either protocol (it never depends on a BiDi-only feature) — see `ROADMAP.md` §2c/§4 for the full write-up.
48
+ WebdriverIO v9 defaults to the WebDriver **BiDi** protocol. A long-lived session that issues many `browser.execute()` calls — exactly what an accessibility scan does — degraded badly over BiDi in testing here: per-scan latency climbed from ~2s into the *minutes* and eventually the session started dropping commands (`Couldn't resolve command with id N`). Forcing the **classic** WebDriver protocol with `'wdio:enforceWebDriverClassic': true` in the capabilities gave a steady ~150ms per scan with no degradation, and every operation this binding needs (`execute`, `$`, `$$`, `switchFrame`) is fully supported on it. This project's own tests and examples all set that flag, and it's recommended for yours. The binding itself works under either protocol (it never depends on a BiDi-only feature).
60
49
 
61
50
  ### Composing the rule filters
62
51
 
63
- `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's default `includeMode: 'and'` — see `../surea11y/docs/ENGINE_OPTIONS.md`), not either one.
52
+ `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.
64
53
 
65
54
  `.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.
66
55
 
@@ -74,7 +63,7 @@ The pattern above works unchanged inside a real WebdriverIO testrunner spec, whi
74
63
 
75
64
  ```js
76
65
  const assert = require('node:assert');
77
- const { A11yCoreBuilder, formatFailures } = require('surea11y-webdriverio');
66
+ const { A11yCoreBuilder, formatFailures } = require('@surea11y/webdriverio');
78
67
 
79
68
  describe('accessibility gate', () => {
80
69
  it('has no accessibility violations', async () => {
@@ -87,7 +76,7 @@ describe('accessibility gate', () => {
87
76
  });
88
77
  ```
89
78
 
90
- 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 — unlike the sibling Puppeteer project, which used `node:test` because Puppeteer ships no first-party runner (`ROADMAP.md` §6 has the full reasoning). This project's own *unit* suite (`tests/*.test.js`) still uses `node:test` + `remote()`, the right tool for exercising the builder API in isolation.
79
+ 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.
91
80
 
92
81
  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.
93
82
 
@@ -104,7 +93,7 @@ A bare length/equality assertion alone gets you a *working* gate, but the failur
104
93
  Add an alt attribute (use alt="" only for decorative images).
105
94
  ```
106
95
 
107
- Deliberately a plain function, not a custom matcher — no dependency on any particular assertion library, so it works the same with `node:assert`, WebdriverIO's `expect`, Jest, Vitest, or a hand-rolled `if`/`throw`. Defaults to `fail`/`cantTell` outcomes (the only two that ever carry occurrences); pass `{ outcomes: [...] }` to narrow further. A thrown rule (`occurrences: []`, `error` set — see `../surea11y/docs/OUTPUT_SCHEMA.md`) is still surfaced using its `error` message rather than silently dropped.
96
+ Deliberately a plain function, not a custom matcher — no dependency on any particular assertion library, so it works the same with `node:assert`, WebdriverIO's `expect`, Jest, Vitest, or a hand-rolled `if`/`throw`. Defaults to `fail`/`cantTell` outcomes (the only two that ever carry occurrences); pass `{ outcomes: [...] }` to narrow further. A thrown rule (`occurrences: []`, `error` set) is still surfaced using its `error` message rather than silently dropped.
108
97
 
109
98
  ### Scanning every frame, including cross-origin and nested iframes
110
99
 
@@ -121,11 +110,11 @@ for (const frame of results.frames) {
121
110
 
122
111
  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.
123
112
 
124
- 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 `ROADMAP.md` §2c and `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 `surea11y` engine support at all.
113
+ 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.
125
114
 
126
115
  ### Trimming the result to just violations
127
116
 
128
- By default `analyze()` returns every rule's outcome, including `pass`/`notApplicable` — surea11y's own deliberate "not a violations-only list" design (see `../surea11y/docs/OUTPUT_SCHEMA.md`). Use `.reportOnly()` to post-filter down to only the outcomes you care about:
117
+ By default `analyze()` returns every rule's outcome, including `pass`/`notApplicable` — `@surea11y/core`'s own deliberate "not a violations-only list" design. Use `.reportOnly()` to post-filter down to only the outcomes you care about:
129
118
 
130
119
  ```js
131
120
  const results = await new A11yCoreBuilder({ browser })
@@ -135,7 +124,7 @@ const results = await new A11yCoreBuilder({ browser })
135
124
  console.log(results.checksResults); // only fail/cantTell entries, pass/notApplicable dropped
136
125
  ```
137
126
 
138
- Valid outcome values are `'pass'`, `'fail'`, `'cantTell'`, `'notApplicable'`. This is pure binding-layer filtering — surea11y 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.
127
+ 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.
139
128
 
140
129
  ### Getting a live element, not just a selector string
141
130
 
@@ -154,13 +143,13 @@ This resolves `occurrence.selector` to a `WebdriverIO.Element` (via `browser.$()
154
143
  Two WebdriverIO-specific differences from the sibling bindings' `.elementRef(true)`, both real and worth knowing:
155
144
 
156
145
  - **The field is `occurrence.element`, not `occurrence.elementHandle`.** WebdriverIO has no "handle" concept — `browser.$()` returns a `WebdriverIO.Element`. Its API differs too: read a property with `.getProperty('id')` (not Puppeteer's `.evaluate(el => el.id)`), screenshot with `.saveScreenshot(path)` (not `.screenshot({ path })`). See the [WebdriverIO element API](https://webdriver.io/docs/api/element).
157
- - **A sub-frame element is only usable while switched into its frame.** WebdriverIO element references are bound to whichever frame was the current context when they were resolved. When you combine `.frames(true).elementRef(true)`, a *sub-frame* occurrence's `element` is resolved against that frame's own document (correct), but after `analyze()` returns the browser is back at the top-level frame — so to act on a sub-frame element you must `browser.switchFrame()` back into its frame first (`browser.switchFrame((await browser.$$('iframe'))[i])`). Top-frame occurrences have no such caveat. This is a direct consequence of WebdriverIO's stateful context model and has no analogue in Puppeteer/Playwright, whose handles are bound to an execution context, not a mutable "current frame." See `ROADMAP.md` §2d.
146
+ - **A sub-frame element is only usable while switched into its frame.** WebdriverIO element references are bound to whichever frame was the current context when they were resolved. When you combine `.frames(true).elementRef(true)`, a *sub-frame* occurrence's `element` is resolved against that frame's own document (correct), but after `analyze()` returns the browser is back at the top-level frame — so to act on a sub-frame element you must `browser.switchFrame()` back into its frame first (`browser.switchFrame((await browser.$$('iframe'))[i])`). Top-frame occurrences have no such caveat. This is a direct consequence of WebdriverIO's stateful context model and has no analogue in Puppeteer/Playwright, whose handles are bound to an execution context, not a mutable "current frame."
158
147
 
159
148
  Not every occurrence has one target element — a page-wide finding (some `manual`/`cantTell` rules) can carry `selector: ""`, in which case `occurrence.element` is `null` rather than an element. (WebdriverIO's `browser.$('')` actually *throws* `invalid selector: No selector specified`, unlike Puppeteer/Playwright's `.$('')` which resolves to `null` — so the empty-selector guard that produces this `null` matters even more here; verified with a real run.)
160
149
 
161
150
  ### Registering a custom rule at runtime
162
151
 
163
- `surea11y` supports registering additional rules per-scan via `engineOptions.customRules`. Use `.withCustomRules()` to register one:
152
+ `@surea11y/core` supports registering additional rules per-scan via `engineOptions.customRules`. Use `.withCustomRules()` to register one:
164
153
 
165
154
  ```js
166
155
  const results = await new A11yCoreBuilder({ browser })
@@ -177,7 +166,7 @@ const results = await new A11yCoreBuilder({ browser })
177
166
  .analyze();
178
167
  ```
179
168
 
180
- A custom rule descriptor is the same shape as one of surea11y's own internal rule modules (`{ id, meta, runInPage, applicability?, data? }`) — see `../surea11y/docs/ENGINE_OPTIONS.md` for the full contract. Results appear in `checksResults` exactly like a built-in rule's, including automatic `selector`/`html`/`structuralPath` fill-in. Registered per-scan only (nothing persists between calls or shows up in any catalog listing), and a custom rule whose `id` collides with a built-in one overrides it for that scan.
169
+ A custom rule descriptor is the same shape as one of `@surea11y/core`'s own internal rule modules (`{ id, meta, runInPage, applicability?, data? }`) — see its [`ENGINE_OPTIONS.md`](https://github.com/rumoroso/surea11y-core/blob/main/docs/ENGINE_OPTIONS.md) for the full contract. Results appear in `checksResults` exactly like a built-in rule's, including automatic `selector`/`html`/`structuralPath` fill-in. Registered per-scan only (nothing persists between calls or shows up in any catalog listing), and a custom rule whose `id` collides with a built-in one overrides it for that scan.
181
170
 
182
171
  Pass an array to register several at once, or call `.withCustomRules()` again to add more — like `.withRules()`/`.withTags()`, it accumulates rather than replacing what was already registered:
183
172
 
@@ -188,31 +177,37 @@ const results = await new A11yCoreBuilder({ browser })
188
177
  .analyze();
189
178
  ```
190
179
 
191
- **Why `.withCustomRules()` instead of the raw `.options({ customRules })` passthrough** (still supported, and composes with this method if you use both): `runInPage`/`applicability` must reach the page as a function-source *string*, not a live `Function` — a WebdriverIO `browser.execute()` argument crosses a serialization boundary that can't carry a live function reference, only a string surea11y can reconstruct with `new Function` on the page side. Passing a raw live function via `.options()` directly would silently fail to serialize; `.withCustomRules()` calls `.toString()` on a live function for you (patching the ES6 method-shorthand `.toString()` quirk automatically — see `ROADMAP.md` §2a), so you can write a normal function and not have to remember that constraint yourself. A string is still accepted as-is if you already have one.
180
+ **Why `.withCustomRules()` instead of the raw `.options({ customRules })` passthrough** (still supported, and composes with this method if you use both): `runInPage`/`applicability` must reach the page as a function-source *string*, not a live `Function` — a WebdriverIO `browser.execute()` argument crosses a serialization boundary that can't carry a live function reference, only a string `@surea11y/core` can reconstruct with `new Function` on the page side. Passing a raw live function via `.options()` directly would silently fail to serialize; `.withCustomRules()` calls `.toString()` on a live function for you (patching the ES6 method-shorthand `.toString()` quirk automatically), so you can write a normal function and not have to remember that constraint yourself. A string is still accepted as-is if you already have one.
192
181
 
193
- Invalid input (a missing/empty `id`, or a `runInPage`/`applicability` that's neither a function nor a non-empty string) throws immediately from `.withCustomRules()` itself, rather than surfacing later as a silently-skipped rule deep inside the page — easier to catch during development. (Note: a *raw* `.options({ customRules })` call bypasses this check entirely and defers to surea11y's own engine-side behavior, which silently skips an invalid descriptor rather than throwing — see `../surea11y/docs/ENGINE_OPTIONS.md`.)
182
+ Invalid input (a missing/empty `id`, or a `runInPage`/`applicability` that's neither a function nor a non-empty string) throws immediately from `.withCustomRules()` itself, rather than surfacing later as a silently-skipped rule deep inside the page — easier to catch during development. (Note: a *raw* `.options({ customRules })` call bypasses this check entirely and defers to `@surea11y/core`'s own engine-side behavior, which silently skips an invalid descriptor rather than throwing.)
194
183
 
195
184
  ### Element addressing beyond a CSS selector
196
185
 
197
- Every occurrence already carries `selector` and (with `.elementRef(true)`, above) a live `element`. It also carries `structuralPath` — a sibling-index path from the document root down to the flagged element (e.g. `[1, 0, 2]`) — a more robust identity than a selector string alone, since it survives some DOM changes a selector wouldn't (an id/class rename, for instance). No opt-in needed; it's already on every `fail`/`cantTell` occurrence today. See `../surea11y/docs/OUTPUT_SCHEMA.md` for the full field description.
186
+ Every occurrence already carries `selector` and (with `.elementRef(true)`, above) a live `element`. It also carries `structuralPath` — a sibling-index path from the document root down to the flagged element (e.g. `[1, 0, 2]`) — a more robust identity than a selector string alone, since it survives some DOM changes a selector wouldn't (an id/class rename, for instance). No opt-in needed; it's already on every `fail`/`cantTell` occurrence today. See [`OUTPUT_SCHEMA.md`](https://github.com/rumoroso/surea11y-core/blob/main/docs/OUTPUT_SCHEMA.md) for the full field description.
198
187
 
199
188
  ## TypeScript
200
189
 
201
- `src/A11yCoreBuilder.d.ts` (re-exported from `src/index.d.ts`, wired up via `package.json`'s `types` field) ships hand-written types for the whole builder API plus surea11y's native result shapes (`A11yCoreResult`, `CheckResult`, `Occurrence`, `CompositeResult`, etc.), mirrored from `../surea11y/docs/OUTPUT_SCHEMA.md`. `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. Verified with a real `tsc --strict` compile against a throwaway consumer script exercising every builder method and both `analyze()` return shapes (see `ROADMAP.md` §4).
190
+ `src/A11yCoreBuilder.d.ts` (re-exported from `src/index.d.ts`, wired up via `package.json`'s `types` field) ships hand-written types for the whole builder API plus `@surea11y/core`'s native result shapes (`A11yCoreResult`, `CheckResult`, `Occurrence`, `CompositeResult`, etc.). `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.
202
191
 
203
- ## Relationship to `surea11y-playwright` and `surea11y-puppeteer`
192
+ ## Relationship to `@surea11y/playwright` and `@surea11y/puppeteer`
204
193
 
205
- This binding's builder API is deliberately the same shape as [`surea11y-playwright`](../surea11y-playwright)'s and [`surea11y-puppeteer`](../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`) used to be copied near-verbatim across every binding; as of `ROADMAP.md` §8 it's no longer copied at all — `A11yCoreBuilder` here extends `A11yCoreBuilderBase` from [`../surea11y-binding-base`](../surea11y-binding-base), the single shared implementation every sibling binding now depends on.
194
+ This binding's builder API is deliberately the same shape as [`@surea11y/playwright`](https://github.com/rumoroso/surea11y-core-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/rumoroso/surea11y-core-binding-base), the single shared implementation every sibling binding depends on.
206
195
 
207
196
  Where it genuinely diverges is WebdriverIO's driver model, and those differences are real, not cosmetic:
208
197
 
209
198
  - **`analyze()`'s injection call** is variadic like Puppeteer's (`browser.execute(runa11yCoreInPage, url, ctx, opts, runOnly)`) — no single-arg wrapper/`eval()` trick the Playwright binding needs.
210
- - **`.frames(true)`** can't iterate a `page.frames()` array (there isn't one) — it drives WebdriverIO's stateful `switchFrame()` context model instead, recursing to reach nested frames. See above and `ROADMAP.md` §2c.
199
+ - **`.frames(true)`** can't iterate a `page.frames()` array (there isn't one) — it drives WebdriverIO's stateful `switchFrame()` context model instead, recursing to reach nested frames. See above.
211
200
  - **`.elementRef(true)`** attaches `occurrence.element` (a `WebdriverIO.Element`), not `occurrence.elementHandle`, with the sub-frame-context caveat described above.
212
- - **Protocol:** classic WebDriver is strongly preferred over WebdriverIO v9's default BiDi for this execute()-heavy workload (see above and `ROADMAP.md` §4).
201
+ - **Protocol:** classic WebDriver is strongly preferred over WebdriverIO v9's default BiDi for this execute()-heavy workload (see above).
202
+
203
+ ## Building another framework binding?
204
+
205
+ See `@surea11y/core`'s [`BINDING_AUTHORS_GUIDE.md`](https://github.com/rumoroso/surea11y-core/blob/main/docs/BINDING_AUTHORS_GUIDE.md) — a reference for building a new binding, covering which parity features are engine-level (work through a generic `.options()`/`runOnly` passthrough with zero binding code, including WCAG-version tag filtering) vs. binding-layer (element refs, `reportOnly`-style verbosity filtering, the serialization-boundary caveat `.withCustomRules()` exists to paper over).
206
+
207
+ `A11yCoreBuilder` here extends `A11yCoreBuilderBase` from [`@surea11y/binding-base`](https://github.com/rumoroso/surea11y-core-binding-base), a small shared package holding the scaffolding common to every framework binding. A new binding should depend on that package from the start.
213
208
 
214
- Also see [`../surea11y/docs/BINDING_AUTHORS_GUIDE.md`](../surea11y/docs/BINDING_AUTHORS_GUIDE.md) — `surea11y`'s own reference for building a binding like this one (it names WebdriverIO explicitly as a candidate next binding), distinguishing what's already engine-level (a generic `.options()`/`runOnly` passthrough, WCAG-version tag filtering, `structuralPath`) from what every binding has to build itself (element refs, `reportOnly`-style verbosity filtering, the serialization-boundary caveat `.withCustomRules()` exists to paper over).
209
+ ## License
215
210
 
216
- ## Status and what's next
211
+ MIT — see [`LICENSE`](./LICENSE).
217
212
 
218
- See `ROADMAP.md` — it documents what's built, what's verified with a real passing run, and what's still open.
213
+ This package depends on [`@surea11y/core`](https://github.com/rumoroso/surea11y-core), which is MPL-2.0. MPL-2.0's copyleft is file-level and applies only to `@surea11y/core`'s own source files; consuming it as a normal package dependency doesn't affect this package's license.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@surea11y/webdriverio",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "WebdriverIO binding for surea11y -- scan real, live-rendered pages for accessibility issues.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -68,7 +68,7 @@ export interface Occurrence {
68
68
  * `WebdriverIO.Element`), not `elementHandle` as in the Puppeteer/Playwright
69
69
  * bindings -- WebdriverIO has no "handle" concept. A sub-frame occurrence's
70
70
  * element is only usable while the browser is switched into that frame; see
71
- * A11yCoreBuilder#elementRef and ../ROADMAP.md §2d.
71
+ * A11yCoreBuilder#elementRef.
72
72
  */
73
73
  element?: WebdriverIO.Element | null;
74
74
  }
@@ -14,8 +14,8 @@ const { A11yCoreBuilderBase } = require('@surea11y/binding-base');
14
14
  * .options({ contrast: { mode: 'auditorAssist' } })
15
15
  * .analyze();
16
16
  *
17
- * `results` is surea11y's own native result shape (checksResults /
18
- * rulesResults -- see surea11y's docs/OUTPUT_SCHEMA.md), not the
17
+ * `results` is @surea11y/core's own native result shape (checksResults /
18
+ * rulesResults -- see ../core/docs/OUTPUT_SCHEMA.md), not the
19
19
  * violations/passes/incomplete/inapplicable shape used by other tools in
20
20
  * this space. Method names are modeled on common conventions in this space
21
21
  * for migration ease, but the richer native schema (severity, confidence,
@@ -27,7 +27,7 @@ const { A11yCoreBuilderBase } = require('@surea11y/binding-base');
27
27
  * be navigated to and settled at the URL to scan; this class does not
28
28
  * navigate for you.
29
29
  *
30
- * Extends `A11yCoreBuilderBase` (from `surea11y-binding-base`), which owns
30
+ * Extends `A11yCoreBuilderBase` (from `@surea11y/binding-base`), which owns
31
31
  * every method with no driver-specific work at all -- `include()`/
32
32
  * `exclude()`/`withTags()`/`disableTags()`/`withRules()`/`disableRules()`/
33
33
  * `options()`/`reportOnly()`/`elementRef()`/`frames()`/`withCustomRules()`'s
@@ -36,7 +36,7 @@ const { A11yCoreBuilderBase } = require('@surea11y/binding-base');
36
36
  * boundary), and `_buildEngineArgs()`. This class adds exactly the parts
37
37
  * that are genuinely WebdriverIO-specific: `analyze()`'s injection
38
38
  * mechanics, the stateful index-path frame-traversal design below, and
39
- * `_attachElementRefs()`. See `../surea11y-binding-base/README.md` for
39
+ * `_attachElementRefs()`. See `../binding-base/README.md` for
40
40
  * what's shared and why.
41
41
  *
42
42
  * Opt in to scanning every frame on the page (including cross-origin
@@ -62,13 +62,13 @@ const { A11yCoreBuilderBase } = require('@surea11y/binding-base');
62
62
  * way in). Cross-origin iframes are reached with no extra work -- the
63
63
  * WebDriver/BiDi automation layer switches into them the same as same-origin
64
64
  * ones, exactly like Puppeteer/Playwright's CDP does; verified against a
65
- * real cross-origin frame (`https://example.org/`). See ../ROADMAP.md §2c.
65
+ * real cross-origin frame (`https://example.org/`).
66
66
  * Default off, so plain .analyze() keeps returning the single native result
67
67
  * object it always has.
68
68
  *
69
69
  * By default `analyze()` returns every rule's outcome, including
70
- * `pass`/`notApplicable` -- surea11y's own deliberate "not a
71
- * violations-only list" design (see surea11y's docs/OUTPUT_SCHEMA.md).
70
+ * `pass`/`notApplicable` -- @surea11y/core's own deliberate "not a
71
+ * violations-only list" design (see ../core/docs/OUTPUT_SCHEMA.md).
72
72
  * Opt in to a lighter payload with `.reportOnly(['fail', 'cantTell'])`,
73
73
  * which post-filters `checksResults` by `outcome` (applied per-frame when
74
74
  * combined with `.frames(true)`, since `checksResults` lives at
@@ -93,15 +93,15 @@ const { A11yCoreBuilderBase } = require('@surea11y/binding-base');
93
93
  * Puppeteer/Playwright's `occurrence.elementHandle` -- WebdriverIO has no
94
94
  * "handle" concept, and its element API differs (`.getProperty('id')` rather
95
95
  * than `.evaluate(el => el.id)`, `.saveScreenshot(path)` rather than
96
- * `.screenshot({ path })`). See ../ROADMAP.md §2d and the README.
96
+ * `.screenshot({ path })`). See the README.
97
97
  *
98
98
  * Register your own rule(s) for just this scan with
99
- * `.withCustomRules([...])` (surea11y's `engineOptions.customRules`
100
- * escape hatch -- see surea11y's docs/ENGINE_OPTIONS.md). Pass a real,
99
+ * `.withCustomRules([...])` (@surea11y/core's `engineOptions.customRules`
100
+ * escape hatch -- see ../core/docs/ENGINE_OPTIONS.md). Pass a real,
101
101
  * live `runInPage`/
102
102
  * `applicability` function -- unlike the raw `.options({ customRules })`
103
103
  * passthrough, this method converts them to the function-source string
104
- * surea11y needs on this side of the browser.execute() serialization
104
+ * @surea11y/core needs on this side of the browser.execute() serialization
105
105
  * boundary for you, so you don't have to remember to call .toString()
106
106
  * yourself:
107
107
  *
@@ -142,8 +142,8 @@ class A11yCoreBuilder extends A11yCoreBuilderBase {
142
142
  }
143
143
 
144
144
  /**
145
- * Runs the scan and returns surea11y's native result object.
146
- * @returns {Promise<object>} see surea11y's docs/OUTPUT_SCHEMA.md
145
+ * Runs the scan and returns @surea11y/core's native result object.
146
+ * @returns {Promise<object>} see ../core/docs/OUTPUT_SCHEMA.md
147
147
  */
148
148
  async analyze() {
149
149
  const { contextSelector, engineOptions, runOnly } = this._buildEngineArgs();
@@ -205,7 +205,7 @@ class A11yCoreBuilder extends A11yCoreBuilderBase {
205
205
  * That's what makes it protocol-agnostic: WebdriverIO's switchFrame()
206
206
  * returns a reusable BiDi context id under the default BiDi protocol but
207
207
  * `null` under the classic WebDriver protocol (`wdio:enforceWebDriverClassic`,
208
- * which this project's own tests/examples use -- see ../ROADMAP.md §2c/§4),
208
+ * which this project's own tests/examples use),
209
209
  * and `$$('iframe')` element references don't survive a context switch away
210
210
  * and back either. The one thing that reliably re-addresses a nested context
211
211
  * on both protocols is replaying `switchFrame(null)` then the chain of
@@ -1,11 +1,10 @@
1
1
  'use strict';
2
2
 
3
- // Re-exported from surea11y-binding-base, which now holds the single
3
+ // Re-exported from @surea11y/binding-base, which now holds the single
4
4
  // canonical copy shared across every surea11y binding -- see
5
- // ../ROADMAP.md's note on the shared-package extraction, and
6
- // ../surea11y-binding-base/README.md for the full rationale. Kept as a
5
+ // ../binding-base/README.md for the full rationale. Kept as a
7
6
  // real file here (not just re-exporting straight from src/index.js) so
8
- // `require('surea11y-webdriverio/src/formatFailures')` keeps working for
7
+ // `require('@surea11y/webdriverio/src/formatFailures')` keeps working for
9
8
  // anyone importing the submodule path directly.
10
9
  const { formatFailures } = require('@surea11y/binding-base');
11
10