@surea11y/webdriverio 1.4.1 → 1.4.2
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 +45 -6
- package/package.json +2 -2
- package/src/A11yCoreBuilder.d.ts +15 -2
- package/src/A11yCoreBuilder.js +87 -9
package/README.md
CHANGED
|
@@ -5,12 +5,12 @@ A WebdriverIO binding for [`@surea11y/core`](https://github.com/SureA11y/core)
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @surea11y/webdriverio webdriverio
|
|
8
|
+
npm install @surea11y/core @surea11y/webdriverio webdriverio
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`@surea11y/core` 1.12.0 or later, the engine, comes with
|
|
11
|
+
`@surea11y/core` 1.12.0 or later, the engine, comes with the binding as a regular dependency, so a project that doesn't list it still works. List it in your own `package.json` too, as the line above does: your project and the binding then share one copy, so whatever you do with core directly, writing a SARIF log or an HTML report (`@surea11y/core/sarif`, `/report`), reading the rule catalogue or keeping a baseline, reads results with the same core that produced them. When your core can't match the binding's range, as with a linked checkout (`file:../core`) or a fork, add `"overrides": { "@surea11y/core": "$@surea11y/core" }` beside it.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
**Coming in 2.0:** core becomes a peer dependency, so the binding will no longer install it for you, and the `overrides` entry is no longer needed. Add `@surea11y/core` to your `package.json` now and 2.0 needs no change.
|
|
14
14
|
|
|
15
15
|
`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.
|
|
16
16
|
|
|
@@ -99,7 +99,7 @@ Rule IDs change rarely, but core 1.10.0 changed two: `label-title-only` is depre
|
|
|
99
99
|
|
|
100
100
|
`.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.
|
|
101
101
|
|
|
102
|
-
**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()`/`.includeMode()`/`.wcagLevel()`/`.logger()` are the exception: each call replaces the previous value instead of merging with it.
|
|
102
|
+
**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()`/`.screenshots()`/`.withWcag()`/`.withBestPractices()`/`.includeMode()`/`.wcagLevel()`/`.logger()` are the exception: each call replaces the previous value instead of merging with it.
|
|
103
103
|
|
|
104
104
|
This binding works against any browser WebdriverIO can drive; the tests and examples use headless Chrome via `chromedriver`, which WebdriverIO manages automatically.
|
|
105
105
|
|
|
@@ -216,6 +216,37 @@ Two WebdriverIO-specific differences from the sibling bindings' `.elementRef(tru
|
|
|
216
216
|
|
|
217
217
|
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.
|
|
218
218
|
|
|
219
|
+
### A picture of each failing element
|
|
220
|
+
|
|
221
|
+
Opt in with `.screenshots()`, and each occurrence the scan flags gets a `screenshot` field: a picture of the element, with a box drawn around it, or the reason there is none.
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
const results = await new A11yCoreBuilder({ browser })
|
|
225
|
+
.screenshots({ dir: 'a11y-shots', padding: 16 })
|
|
226
|
+
.analyze();
|
|
227
|
+
|
|
228
|
+
const [first] = results.checksResults.filter((r) => r.outcome === 'fail');
|
|
229
|
+
first.occurrences[0].screenshot;
|
|
230
|
+
// { path: 'a11y-shots/element-3f9a1c07.png', box: { x: 20, y: 20, width: 120, height: 40 } }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
| Option | Default | |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `dir` | none | Write each picture there. Without it, each comes back as `{ buffer, box }`, a PNG for you to store your own way. |
|
|
236
|
+
| `outcomes` | `['fail']` | Which occurrences to picture. For contrast, `['fail', 'cantTell']` shows the background the engine couldn't judge. |
|
|
237
|
+
| `padding` | `0` | CSS pixels of context around the element. |
|
|
238
|
+
| `highlight` | `true` | Draw a box around the element, over the page; the element itself is never restyled. |
|
|
239
|
+
| `max` | `50` | At most this many pictures per scan, frames included. |
|
|
240
|
+
|
|
241
|
+
- **One picture per element.** Several rules can flag the same element; it's pictured once, and each of their occurrences carries the same `screenshot`. A file is named after where the element is, so it keeps its name from run to run, and `formatFailures()` names it under each finding (` screenshot: a11y-shots/element-3f9a1c07.png`).
|
|
242
|
+
- **`box`** is where the element shows in the picture, in CSS pixels; the PNG is larger by the device pixel ratio. `cropped: true` says only part of it shows, as for an element taller than the viewport, which is pictured from its top-left corner.
|
|
243
|
+
- **No picture, and why**: `{ skipped }` with `'no-element'` (no selector, or nothing matches it any more), `'page-level'` (a finding about the whole page, on `<html>`, `<head>` or `<body>`), `'not-rendered'` (such as `display: none`), `'zero-size'`, `'off-screen'` (such as `left: -9999px`), `'max-reached'`, or `'capture-failed'` with the reason. A failed picture never fails the scan.
|
|
244
|
+
- **The page is put back.** Each element is scrolled into view for its picture, and afterwards every scroll position is restored and the box removed, also when a picture fails. Something drawn over the element, such as a sticky header, shows as it does on screen.
|
|
245
|
+
- **Frames.** With `.frames(true)`, an element in an `<iframe>`, cross-origin ones included, is pictured on the page as it shows there, and the browser ends at the top-level page as before. Without it, the scan runs in whatever frame the browser is switched into; pictures are placed from the top-level page, so a scan started inside a frame gives its occurrences `capture-failed` and says why. Scan from the top-level page with `.frames(true)` to picture a frame's elements.
|
|
246
|
+
- **Off by default.** Pictures of real pages can hold personal data, and they end up as CI artifacts.
|
|
247
|
+
|
|
248
|
+
The pictures are cut out of WebDriver's screenshot of the viewport (`browser.takeScreenshot()`), right after each frame's scan, before `.reportOnly()` trims the result, and combine with `.elementRef(true)`. Focus, hover, animations or content that loads late can still differ from what the scan saw. An option it can't use throws a `TypeError` with `code: 'INVALID_ENGINE_OPTIONS'` at the call, and so does `.analyze()` with `options({ output: { includeSelector: false } })`, since each element is found by its selector. Works over WebDriver classic and BiDi.
|
|
249
|
+
|
|
219
250
|
### Web components: the internals hook
|
|
220
251
|
|
|
221
252
|
Many web components keep their `ElementInternals` in a private field and name themselves, set their states or give themselves a role through it:
|
|
@@ -368,6 +399,14 @@ results.engine.profile; // 'en301549-v4.1.1'
|
|
|
368
399
|
|
|
369
400
|
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.
|
|
370
401
|
|
|
402
|
+
Install a published pack in your project, beside core and this package:
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
npm install @surea11y/core @surea11y/webdriverio webdriverio @acme/a11y-policy
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Your project, this package and the pack then share one copy of core, so the core that checks the pack is the one that runs it. With two copies, a scan can skip the pack (see "When a pack doesn't run" below).
|
|
409
|
+
|
|
371
410
|
`.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 })`:
|
|
372
411
|
|
|
373
412
|
```js
|
|
@@ -389,7 +428,7 @@ A pack's own rules appear in `checksResults` like core's, with ids that start wi
|
|
|
389
428
|
|
|
390
429
|
**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.
|
|
391
430
|
|
|
392
|
-
**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.
|
|
431
|
+
**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.
|
|
393
432
|
|
|
394
433
|
### Element addressing beyond a CSS selector
|
|
395
434
|
|
|
@@ -399,7 +438,7 @@ An element inside a shadow tree is different: its `selector` holds only inside i
|
|
|
399
438
|
|
|
400
439
|
## TypeScript
|
|
401
440
|
|
|
402
|
-
`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`, `ScanGap` and `ScanNote` come from `@surea11y/binding-base`, and so do the types of `withWcag()` and `wcagLevel()` (`WcagVersion`, `WcagLevelInput`), `plannedChecks()`/`plannedCriteria()` (`PlannedCheck`, `PlannedCriterion`),
|
|
441
|
+
`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`, `ScanGap` and `ScanNote` come from `@surea11y/binding-base`, and so do the types of `withWcag()` and `wcagLevel()` (`WcagVersion`, `WcagLevelInput`), `plannedChecks()`/`plannedCriteria()` (`PlannedCheck`, `PlannedCriterion`), `withPacks()`, which takes a pack made with `definePack` (`PackLike`), and `.screenshots()` (`ScreenshotOptions`, and `OccurrenceScreenshot` for an occurrence's `screenshot`). The static `installInternalsHook()` and `getScanNotes()` are typed too. 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.
|
|
403
442
|
|
|
404
443
|
## What changes with `@surea11y/core` 1.12
|
|
405
444
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surea11y/webdriverio",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Accessibility (a11y, WCAG) checks for WebdriverIO tests: scan the live page with new A11yCoreBuilder({ browser }).analyze() and get only proven violations, with findings that need human review listed separately.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"LICENSE"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@surea11y/binding-base": "^1.
|
|
50
|
+
"@surea11y/binding-base": "^1.5.0",
|
|
51
51
|
"@surea11y/core": "^1.12.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
package/src/A11yCoreBuilder.d.ts
CHANGED
|
@@ -10,8 +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, WcagLevelInput, ScanGap, ScanNote, PlannedCheck, PlannedCriterion } from '@surea11y/binding-base';
|
|
14
|
-
export type { PackLike, WcagVersion, WcagLevel } from '@surea11y/binding-base';
|
|
13
|
+
import type { PackLike, WcagVersion, WcagLevelInput, ScanGap, ScanNote, PlannedCheck, PlannedCriterion, ScreenshotOptions, OccurrenceScreenshot } from '@surea11y/binding-base';
|
|
14
|
+
export type { PackLike, WcagVersion, WcagLevel, ScreenshotOptions, ScreenshotSkip, ScreenshotBox, OccurrenceScreenshot } from '@surea11y/binding-base';
|
|
15
15
|
|
|
16
16
|
export type Outcome = Core.Outcome;
|
|
17
17
|
export type OutcomeNormalized = Core.OutcomeNormalized;
|
|
@@ -53,6 +53,12 @@ export interface Occurrence extends Core.Occurrence {
|
|
|
53
53
|
* switched into that frame.
|
|
54
54
|
*/
|
|
55
55
|
element?: WebdriverIO.Element | null;
|
|
56
|
+
/**
|
|
57
|
+
* Only present when `.screenshots()` was used and this occurrence's own
|
|
58
|
+
* outcome is one it pictures: the picture, or why there is none. The
|
|
59
|
+
* occurrences about the same element share one object.
|
|
60
|
+
*/
|
|
61
|
+
screenshot?: OccurrenceScreenshot;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export interface CheckResult extends Omit<Core.CheckResult, 'occurrences'> {
|
|
@@ -190,6 +196,13 @@ export class A11yCoreBuilder {
|
|
|
190
196
|
frames(enabled?: boolean): this;
|
|
191
197
|
/** Opt in to resolving each fail/cantTell occurrence's selector to a live WebdriverIO.Element. */
|
|
192
198
|
elementRef(enabled?: boolean): this;
|
|
199
|
+
/**
|
|
200
|
+
* Opt in to a picture of each element the scan flags, on its occurrences'
|
|
201
|
+
* `screenshot` field: a Buffer, or a file in `dir`. `false` turns them
|
|
202
|
+
* off. Throws a TypeError with code 'INVALID_ENGINE_OPTIONS' for an option
|
|
203
|
+
* it can't use.
|
|
204
|
+
*/
|
|
205
|
+
screenshots(options?: ScreenshotOptions | boolean): this;
|
|
193
206
|
/** The rules this scan will run, worked out in Node with no page (@surea11y/core 1.12.0 and later). */
|
|
194
207
|
plannedChecks(): PlannedCheck[];
|
|
195
208
|
/** The criteria this scan will report in rulesResults, worked out in Node with no page. */
|
package/src/A11yCoreBuilder.js
CHANGED
|
@@ -5,7 +5,9 @@ const {
|
|
|
5
5
|
A11yCoreBuilderBase,
|
|
6
6
|
createInPageScan,
|
|
7
7
|
rethrowEngineError,
|
|
8
|
-
queryOccurrenceElement
|
|
8
|
+
queryOccurrenceElement,
|
|
9
|
+
planScreenshots,
|
|
10
|
+
cropViewportScreenshot
|
|
9
11
|
} = require('@surea11y/binding-base');
|
|
10
12
|
|
|
11
13
|
// core's runa11yCoreInPage, wrapped so an engine error (INVALID_RUN_ONLY,
|
|
@@ -112,6 +114,19 @@ const inPageScan = createInPageScan(runa11yCoreInPage);
|
|
|
112
114
|
* than `.evaluate(el => el.id)`, `.saveScreenshot(path)` rather than
|
|
113
115
|
* `.screenshot({ path })`). See the README.
|
|
114
116
|
*
|
|
117
|
+
* Or let the scan take a picture of each element it flags, with
|
|
118
|
+
* `.screenshots()` (@surea11y/binding-base's, see its README): each
|
|
119
|
+
* occurrence gets a `screenshot` field, the picture (a Buffer, or a file in
|
|
120
|
+
* `dir`) or why there is none. The element is scrolled into view and marked
|
|
121
|
+
* with a box drawn over the page, and the page is put back afterwards; in
|
|
122
|
+
* every frame with `.frames(true)`, including cross-origin ones:
|
|
123
|
+
*
|
|
124
|
+
* const results = await new A11yCoreBuilder({ browser })
|
|
125
|
+
* .screenshots({ dir: 'a11y-shots', padding: 16 })
|
|
126
|
+
* .analyze();
|
|
127
|
+
* // results.checksResults[i].occurrences[j].screenshot
|
|
128
|
+
* // -> { path: 'a11y-shots/element-3f9a1c07.png', box: { x, y, width, height } }
|
|
129
|
+
*
|
|
115
130
|
* Register your own rule(s) for just this scan with
|
|
116
131
|
* `.withCustomRules([...])` (@surea11y/core's `engineOptions.customRules`
|
|
117
132
|
* escape hatch -- see ../core/docs/ENGINE_OPTIONS.md). Pass a real,
|
|
@@ -141,8 +156,9 @@ const inPageScan = createInPageScan(runa11yCoreInPage);
|
|
|
141
156
|
* top of the first scan's scope rather than replacing it (intentional for
|
|
142
157
|
* "call include() several times for one scan" -- see above -- but a footgun
|
|
143
158
|
* if you hold one instance across multiple assertions).
|
|
144
|
-
* reportOnly()/frames()/elementRef()/withWcag()/withBestPractices()
|
|
145
|
-
* exception: each call replaces the previous value rather than
|
|
159
|
+
* reportOnly()/frames()/elementRef()/screenshots()/withWcag()/withBestPractices()
|
|
160
|
+
* are the exception: each call replaces the previous value rather than
|
|
161
|
+
* merging with it.
|
|
146
162
|
*/
|
|
147
163
|
class A11yCoreBuilder extends A11yCoreBuilderBase {
|
|
148
164
|
/**
|
|
@@ -236,28 +252,38 @@ class A11yCoreBuilder extends A11yCoreBuilderBase {
|
|
|
236
252
|
// scan names in engineOptions.packs; null without packs. execute() runs
|
|
237
253
|
// a string as a function body, so the script runs as written.
|
|
238
254
|
const packScript = this._packScript();
|
|
239
|
-
|
|
255
|
+
// `path` is the frame being scanned, as _scanChildFrames addresses it
|
|
256
|
+
// ([] for the top-level page); the screenshots step needs it to reach
|
|
257
|
+
// the frame again, and its parents.
|
|
258
|
+
const scanCurrent = async (scope, path) => {
|
|
240
259
|
const frameUrl = this._url || (await this._browser.execute(() => document.location.href));
|
|
241
260
|
if (packScript) await this._browser.execute(packScript);
|
|
242
261
|
const result = rethrowEngineError(
|
|
243
262
|
await this._browser.execute(inPageScan, frameUrl, scope, engineOptions, runOnly)
|
|
244
263
|
);
|
|
245
264
|
this._warnScanGaps(result, '@surea11y/webdriverio');
|
|
246
|
-
|
|
265
|
+
// Element references first: they are resolved in the current frame,
|
|
266
|
+
// which the screenshots step switches away from.
|
|
267
|
+
if (this._elementRef) await this._attachElementRefs(result);
|
|
268
|
+
if (this._screenshots) await this._screenshotFrame(result, path);
|
|
269
|
+
return result;
|
|
247
270
|
};
|
|
248
271
|
|
|
249
272
|
if (!this._scanFrames) {
|
|
250
|
-
|
|
273
|
+
// Whatever frame the caller left the browser in; the screenshots step
|
|
274
|
+
// can place pictures only from the top-level page (see
|
|
275
|
+
// _screenshotFrame), and null says so.
|
|
276
|
+
return this._applyReportOnly(await scanCurrent(contextSelector, null));
|
|
251
277
|
}
|
|
252
278
|
|
|
253
279
|
// The top frame is scanned first, so an INVALID_RUN_ONLY or
|
|
254
280
|
// INVALID_CONTEXT_SELECTOR error is thrown from it, before any sub-frame.
|
|
255
|
-
const topFrame = this._applyReportOnly(await scanCurrent(contextSelector));
|
|
281
|
+
const topFrame = this._applyReportOnly(await scanCurrent(contextSelector, []));
|
|
256
282
|
|
|
257
283
|
// Sub-frames are scanned whole: the include() scope belongs to the top
|
|
258
284
|
// frame's document (see the class comment).
|
|
259
285
|
const frames = [];
|
|
260
|
-
await this._scanChildFrames([], () => scanCurrent(null), frames);
|
|
286
|
+
await this._scanChildFrames([], (childPath) => scanCurrent(null, childPath), frames);
|
|
261
287
|
|
|
262
288
|
// Leave the browser back at the top-level frame regardless of how the
|
|
263
289
|
// traversal ended, so a caller's next command isn't silently running in
|
|
@@ -315,7 +341,7 @@ class A11yCoreBuilder extends A11yCoreBuilderBase {
|
|
|
315
341
|
}
|
|
316
342
|
|
|
317
343
|
try {
|
|
318
|
-
out.push(this._applyReportOnly(await scanCurrent()));
|
|
344
|
+
out.push(this._applyReportOnly(await scanCurrent(path.concat(i))));
|
|
319
345
|
} catch (e) {
|
|
320
346
|
out.push({ url: frameUrl, error: (e && e.message) || String(e) });
|
|
321
347
|
}
|
|
@@ -339,6 +365,58 @@ class A11yCoreBuilder extends A11yCoreBuilderBase {
|
|
|
339
365
|
}
|
|
340
366
|
}
|
|
341
367
|
|
|
368
|
+
/**
|
|
369
|
+
* binding-base's screenshots step for the frame at `path` (child <iframe>
|
|
370
|
+
* indices from the top, as _scanChildFrames walks them), with the
|
|
371
|
+
* <iframe>s leading to it given by index among `$$('iframe')`, the way
|
|
372
|
+
* this binding counts frames: element references don't survive switching
|
|
373
|
+
* away and back under BiDi. The step switches between frames as it goes,
|
|
374
|
+
* so the browser is switched back into the scanned frame afterwards, where
|
|
375
|
+
* the traversal goes on.
|
|
376
|
+
*
|
|
377
|
+
* `path` is null for a scan without .frames(true), which runs in whatever
|
|
378
|
+
* frame the caller left the browser in. From the top-level page that is
|
|
379
|
+
* `[]`; from inside a frame, nothing says how it sits on the page, so its
|
|
380
|
+
* occurrences get `capture-failed` rather than misplaced pictures.
|
|
381
|
+
*/
|
|
382
|
+
async _screenshotFrame(result, path) {
|
|
383
|
+
if (path === null) {
|
|
384
|
+
const atTop = await this._browser.execute(() => window === window.top).catch(() => false);
|
|
385
|
+
if (!atTop) {
|
|
386
|
+
const error = 'the browser was switched into a frame when analyze() ran; pictures are placed from the top-level page, so scan from there, with .frames(true) to include the frame';
|
|
387
|
+
const { elements, unlocated } = planScreenshots(result, { outcomes: this._screenshots.outcomes });
|
|
388
|
+
for (const element of elements) {
|
|
389
|
+
const shot = { skipped: 'capture-failed', error };
|
|
390
|
+
for (const occurrence of element.occurrences) occurrence.screenshot = shot;
|
|
391
|
+
}
|
|
392
|
+
for (const occurrence of unlocated) occurrence.screenshot = { skipped: 'no-element' };
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
path = [];
|
|
396
|
+
}
|
|
397
|
+
const framePath = path.map((index, depth) => ({ frame: path.slice(0, depth), iframe: { selector: 'iframe', index } }));
|
|
398
|
+
try {
|
|
399
|
+
await this._captureScreenshots(result, path, framePath);
|
|
400
|
+
} finally {
|
|
401
|
+
await this._switchToPath(path).catch(() => {});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** For binding-base's screenshots step: runs `fn(...args)` in the frame at `path`. */
|
|
406
|
+
async _evaluateInFrame(path, fn, args) {
|
|
407
|
+
await this._switchToPath(path);
|
|
408
|
+
return this._browser.execute(fn, ...args);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* For binding-base's screenshots step: a PNG of `box`, CSS pixels in the
|
|
413
|
+
* top-level viewport, cut out of WebDriver's screenshot of that viewport,
|
|
414
|
+
* which it takes whatever frame the browser is switched into.
|
|
415
|
+
*/
|
|
416
|
+
async _captureBox(box, view) {
|
|
417
|
+
return cropViewportScreenshot(await this._browser.takeScreenshot(), box, view);
|
|
418
|
+
}
|
|
419
|
+
|
|
342
420
|
/**
|
|
343
421
|
* Resolves each occurrence to a live WebdriverIO.Element, scoped to
|
|
344
422
|
* whatever frame the browser is currently switched into (execute() is
|