@axe-core/webdriverjs 4.3.3-alpha.243 → 4.3.3
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/.eslintrc.js +13 -0
- package/README.md +2 -24
- package/dist/axe-injector.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +9 -11
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -3
- package/dist/utils/index.d.ts +1 -2
- package/dist/utils/index.js +11 -9
- package/dist/utils/index.js.map +1 -1
- package/error-handling.md +52 -0
- package/example-with-callback.js +15 -0
- package/example.js +13 -0
- package/package.json +10 -13
- package/src/axe-injector.ts +191 -0
- package/src/browser.ts +142 -0
- package/src/index.ts +262 -0
- package/src/types.ts +27 -0
- package/src/utils/index.ts +23 -0
- package/tests/axe-webdriverjs.spec.ts +676 -0
- package/tests/fixtures/context.html +11 -0
- package/tests/test-utils.ts +49 -0
- package/tsconfig.json +15 -0
package/src/browser.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AxeResults,
|
|
3
|
+
ContextObject,
|
|
4
|
+
FrameContext,
|
|
5
|
+
RunOptions,
|
|
6
|
+
Spec,
|
|
7
|
+
PartialResult
|
|
8
|
+
} from 'axe-core';
|
|
9
|
+
import { WebDriver, WebElement } from 'selenium-webdriver';
|
|
10
|
+
|
|
11
|
+
type FrameContextWeb = FrameContext & {
|
|
12
|
+
frame: WebElement;
|
|
13
|
+
href: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// https://github.com/vercel/pkg/issues/676
|
|
17
|
+
// we need to pass a string vs a function so we manually stringified the function
|
|
18
|
+
// There are no try/catch blocks needed in these scripts. If an error occurs
|
|
19
|
+
// Selenium pass them onto the catch block.
|
|
20
|
+
|
|
21
|
+
export function axeSourceInject(
|
|
22
|
+
driver: WebDriver,
|
|
23
|
+
axeSource: string,
|
|
24
|
+
config: Spec | null
|
|
25
|
+
): Promise<{ runPartialSupported: boolean }> {
|
|
26
|
+
return promisify(
|
|
27
|
+
driver.executeScript<{ runPartialSupported: boolean }>(`
|
|
28
|
+
${axeSource};
|
|
29
|
+
window.axe.configure({
|
|
30
|
+
branding: { application: 'webdriverjs' }
|
|
31
|
+
});
|
|
32
|
+
var config = ${JSON.stringify(config)};
|
|
33
|
+
if (config) {
|
|
34
|
+
window.axe.configure(config);
|
|
35
|
+
}
|
|
36
|
+
var runPartial = typeof window.axe.runPartial === 'function';
|
|
37
|
+
return { runPartialSupported: runPartial };
|
|
38
|
+
`)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function axeRunPartial(
|
|
43
|
+
driver: WebDriver,
|
|
44
|
+
context: ContextObject,
|
|
45
|
+
options: RunOptions
|
|
46
|
+
): Promise<string> {
|
|
47
|
+
return promisify(
|
|
48
|
+
driver.executeAsyncScript<string>(`
|
|
49
|
+
var callback = arguments[arguments.length - 1];
|
|
50
|
+
var context = ${JSON.stringify(context)} || document;
|
|
51
|
+
var options = ${JSON.stringify(options)} || {};
|
|
52
|
+
window.axe.runPartial(context, options).then(res => JSON.stringify(res)).then(callback);
|
|
53
|
+
`)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function axeFinishRun(
|
|
58
|
+
driver: WebDriver,
|
|
59
|
+
axeSource: string,
|
|
60
|
+
config: Spec | null,
|
|
61
|
+
partialResults: Array<string>,
|
|
62
|
+
options: RunOptions
|
|
63
|
+
): Promise<AxeResults> {
|
|
64
|
+
// Inject source and configuration a second time with a mock "this" context,
|
|
65
|
+
// to make it impossible to sniff the global window.axe for results.
|
|
66
|
+
return promisify(
|
|
67
|
+
driver
|
|
68
|
+
.executeAsyncScript<string>(
|
|
69
|
+
`
|
|
70
|
+
var callback = arguments[arguments.length - 1];
|
|
71
|
+
|
|
72
|
+
${axeSource};
|
|
73
|
+
window.axe.configure({
|
|
74
|
+
branding: { application: 'webdriverjs' }
|
|
75
|
+
});
|
|
76
|
+
var config = ${JSON.stringify(config)};
|
|
77
|
+
if (config) {
|
|
78
|
+
window.axe.configure(config);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
var partialResults = ${JSON.stringify(partialResults)};
|
|
82
|
+
partialResults = partialResults.map(res => JSON.parse(res));
|
|
83
|
+
var options = ${JSON.stringify(options || {})};
|
|
84
|
+
window.axe.finishRun(partialResults, options).then(res => JSON.stringify(res)).then(callback);
|
|
85
|
+
`
|
|
86
|
+
)
|
|
87
|
+
.then(res => JSON.parse(res))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function axeGetFrameContext(
|
|
92
|
+
driver: WebDriver,
|
|
93
|
+
context: ContextObject
|
|
94
|
+
): Promise<FrameContextWeb[]> {
|
|
95
|
+
return promisify(
|
|
96
|
+
driver.executeScript<FrameContextWeb[]>(`
|
|
97
|
+
var context = ${JSON.stringify(context)}
|
|
98
|
+
var frameContexts = window.axe.utils.getFrameContexts(context);
|
|
99
|
+
return frameContexts.map(function (frameContext) {
|
|
100
|
+
return Object.assign(frameContext, {
|
|
101
|
+
href: window.location.href, // For debugging
|
|
102
|
+
frame: axe.utils.shadowSelect(frameContext.frameSelector)
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
`)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function axeRunLegacy(
|
|
110
|
+
driver: WebDriver,
|
|
111
|
+
context: ContextObject,
|
|
112
|
+
options: RunOptions,
|
|
113
|
+
config: Spec | null
|
|
114
|
+
): Promise<AxeResults> {
|
|
115
|
+
// https://github.com/vercel/pkg/issues/676
|
|
116
|
+
// we need to pass a string vs a function so we manually stringified the function
|
|
117
|
+
return promisify(
|
|
118
|
+
driver
|
|
119
|
+
.executeAsyncScript<string>(
|
|
120
|
+
`
|
|
121
|
+
var callback = arguments[arguments.length - 1];
|
|
122
|
+
var context = ${JSON.stringify(context)} || document;
|
|
123
|
+
var options = ${JSON.stringify(options)} || {};
|
|
124
|
+
var config = ${JSON.stringify(config)} || null;
|
|
125
|
+
if (config) {
|
|
126
|
+
window.axe.configure(config);
|
|
127
|
+
}
|
|
128
|
+
window.axe.run(context, options).then(res => JSON.stringify(res)).then(callback);
|
|
129
|
+
`
|
|
130
|
+
)
|
|
131
|
+
.then(res => JSON.parse(res))
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Selenium-webdriver thenable aren't chainable. This fixes it.
|
|
137
|
+
*/
|
|
138
|
+
function promisify<T>(thenable: Promise<T>): Promise<T> {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
thenable.then(resolve, reject);
|
|
141
|
+
});
|
|
142
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { WebDriver } from 'selenium-webdriver';
|
|
2
|
+
import { RunOptions, Spec, AxeResults, ContextObject } from 'axe-core';
|
|
3
|
+
import { source } from 'axe-core';
|
|
4
|
+
import { CallbackFunction, BuilderOptions, PartialResults } from './types';
|
|
5
|
+
import { normalizeContext } from './utils/index';
|
|
6
|
+
import AxeInjector from './axe-injector';
|
|
7
|
+
import {
|
|
8
|
+
axeGetFrameContext,
|
|
9
|
+
axeRunPartial,
|
|
10
|
+
axeRunLegacy,
|
|
11
|
+
axeSourceInject,
|
|
12
|
+
axeFinishRun
|
|
13
|
+
} from './browser';
|
|
14
|
+
import * as assert from 'assert';
|
|
15
|
+
|
|
16
|
+
class AxeBuilder {
|
|
17
|
+
private driver: WebDriver;
|
|
18
|
+
private axeSource: string;
|
|
19
|
+
private includes: string[];
|
|
20
|
+
private excludes: string[];
|
|
21
|
+
private option: RunOptions;
|
|
22
|
+
private config: Spec | null;
|
|
23
|
+
private builderOptions: BuilderOptions;
|
|
24
|
+
private legacyMode = false;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
driver: WebDriver,
|
|
28
|
+
axeSource?: string | null,
|
|
29
|
+
builderOptions?: BuilderOptions
|
|
30
|
+
) {
|
|
31
|
+
this.driver = driver;
|
|
32
|
+
this.axeSource = axeSource || source;
|
|
33
|
+
this.includes = [];
|
|
34
|
+
this.excludes = [];
|
|
35
|
+
this.option = {};
|
|
36
|
+
this.config = null;
|
|
37
|
+
this.builderOptions = builderOptions || {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Selector to include in analysis.
|
|
42
|
+
* This may be called any number of times.
|
|
43
|
+
*/
|
|
44
|
+
public include(selector: string): this {
|
|
45
|
+
this.includes.push(selector);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Selector to exclude in analysis.
|
|
51
|
+
* This may be called any number of times.
|
|
52
|
+
*/
|
|
53
|
+
public exclude(selector: string): this {
|
|
54
|
+
this.excludes.push(selector);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Set options to be passed into axe-core
|
|
60
|
+
*/
|
|
61
|
+
public options(options: RunOptions): this {
|
|
62
|
+
this.option = options;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Limit analysis to only the specified rules.
|
|
68
|
+
* Cannot be used with `AxeBuilder#withTags`
|
|
69
|
+
*/
|
|
70
|
+
public withRules(rules: string | string[]): this {
|
|
71
|
+
rules = Array.isArray(rules) ? rules : [rules];
|
|
72
|
+
this.option.runOnly = {
|
|
73
|
+
type: 'rule',
|
|
74
|
+
values: rules
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Limit analysis to only specified tags.
|
|
82
|
+
* Cannot be used with `AxeBuilder#withRules`
|
|
83
|
+
*/
|
|
84
|
+
public withTags(tags: string | string[]): this {
|
|
85
|
+
tags = Array.isArray(tags) ? tags : [tags];
|
|
86
|
+
this.option.runOnly = {
|
|
87
|
+
type: 'tag',
|
|
88
|
+
values: tags
|
|
89
|
+
};
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Set the list of rules to skip when running an analysis.
|
|
95
|
+
*/
|
|
96
|
+
public disableRules(rules: string | string[]): this {
|
|
97
|
+
rules = Array.isArray(rules) ? rules : [rules];
|
|
98
|
+
this.option.rules = {};
|
|
99
|
+
for (const rule of rules) {
|
|
100
|
+
this.option.rules[rule] = { enabled: false };
|
|
101
|
+
}
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Set configuration for `axe-core`.
|
|
107
|
+
* This value is passed directly to `axe.configure()`
|
|
108
|
+
*/
|
|
109
|
+
public configure(config: Spec): this {
|
|
110
|
+
if (typeof config !== 'object') {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'AxeBuilder needs an object to configure. See axe-core configure API.'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
this.config = config;
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Performs an analysis and retrieves results.
|
|
121
|
+
*/
|
|
122
|
+
public async analyze(callback?: CallbackFunction): Promise<AxeResults> {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
return this.analyzePromise()
|
|
125
|
+
.then((results: AxeResults) => {
|
|
126
|
+
callback?.(null, results);
|
|
127
|
+
resolve(results);
|
|
128
|
+
})
|
|
129
|
+
.catch((err: Error) => {
|
|
130
|
+
// When using a callback, do *not* reject the wrapping Promise. This prevents having to handle the same error twice.
|
|
131
|
+
if (callback) {
|
|
132
|
+
callback(err.message, null);
|
|
133
|
+
} else {
|
|
134
|
+
reject(err);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Use frameMessenger with <same_origin_only>
|
|
142
|
+
*
|
|
143
|
+
* This disables use of axe.runPartial() which is called in each frame, and
|
|
144
|
+
* axe.finishRun() which is called in a blank page. This uses axe.run() instead,
|
|
145
|
+
* but with the restriction that cross-origin frames will not be tested.
|
|
146
|
+
*/
|
|
147
|
+
public setLegacyMode(legacyMode = true): AxeBuilder {
|
|
148
|
+
this.legacyMode = legacyMode;
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Analyzes the page, returning a promise
|
|
154
|
+
*/
|
|
155
|
+
private async analyzePromise(): Promise<AxeResults> {
|
|
156
|
+
const context = normalizeContext(this.includes, this.excludes);
|
|
157
|
+
await this.driver.switchTo().defaultContent();
|
|
158
|
+
const { runPartialSupported } = await axeSourceInject(
|
|
159
|
+
this.driver,
|
|
160
|
+
this.axeSource,
|
|
161
|
+
this.config
|
|
162
|
+
);
|
|
163
|
+
if (runPartialSupported !== true || this.legacyMode) {
|
|
164
|
+
return this.runLegacy(context);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const partials = await this.runPartialRecursive(context, true);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
return await this.finishRun(partials);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`${error.message}\n Please check out https://github.com/dequelabs/axe-core-npm/blob/develop/packages/webdriverjs/error-handling.md`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Use axe.run() to get results from the page
|
|
180
|
+
*/
|
|
181
|
+
private async runLegacy(context: ContextObject): Promise<AxeResults> {
|
|
182
|
+
const { driver, axeSource, builderOptions } = this;
|
|
183
|
+
let config = this.config;
|
|
184
|
+
if (!this.legacyMode) {
|
|
185
|
+
config = {
|
|
186
|
+
...(config || {}),
|
|
187
|
+
allowedOrigins: ['<unsafe_all_origins>']
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const injector = new AxeInjector({
|
|
191
|
+
driver,
|
|
192
|
+
axeSource,
|
|
193
|
+
config,
|
|
194
|
+
builderOptions
|
|
195
|
+
});
|
|
196
|
+
await injector.injectIntoAllFrames();
|
|
197
|
+
return axeRunLegacy(this.driver, context, this.option, this.config);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get partial results from the current context and its child frames
|
|
202
|
+
*/
|
|
203
|
+
private async runPartialRecursive(
|
|
204
|
+
context: ContextObject,
|
|
205
|
+
initiator = false
|
|
206
|
+
): Promise<string[]> {
|
|
207
|
+
if (!initiator) {
|
|
208
|
+
await axeSourceInject(this.driver, this.axeSource, this.config);
|
|
209
|
+
}
|
|
210
|
+
// IMPORTANT: axeGetFrameContext MUST be called before axeRunPartial
|
|
211
|
+
const frameContexts = await axeGetFrameContext(this.driver, context);
|
|
212
|
+
const partials: string[] = [
|
|
213
|
+
await axeRunPartial(this.driver, context, this.option)
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
for (const { frameContext, frameSelector, frame } of frameContexts) {
|
|
217
|
+
let switchedFrame = false;
|
|
218
|
+
try {
|
|
219
|
+
assert(frame, `Expect frame of "${frameSelector}" to be defined`);
|
|
220
|
+
await this.driver.switchTo().frame(frame);
|
|
221
|
+
switchedFrame = true;
|
|
222
|
+
partials.push(...(await this.runPartialRecursive(frameContext)));
|
|
223
|
+
await this.driver.switchTo().parentFrame();
|
|
224
|
+
} catch {
|
|
225
|
+
if (switchedFrame) {
|
|
226
|
+
await this.driver.switchTo().parentFrame();
|
|
227
|
+
}
|
|
228
|
+
partials.push('null');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return partials;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Use axe.finishRun() to turn partial results into actual results
|
|
236
|
+
*/
|
|
237
|
+
private async finishRun(partials: string[]): Promise<AxeResults> {
|
|
238
|
+
const { driver, axeSource, config, option } = this;
|
|
239
|
+
|
|
240
|
+
const win = await driver.getWindowHandle();
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await driver.executeScript(`window.open('about:blank')`);
|
|
244
|
+
const handlers = await driver.getAllWindowHandles();
|
|
245
|
+
await driver.switchTo().window(handlers[handlers.length - 1]);
|
|
246
|
+
await driver.get('about:blank');
|
|
247
|
+
} catch (error) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`switchTo failed. Are you using updated browser drivers? \nDriver reported:\n${error}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
// Make sure we're on a blank page, even if window.open isn't functioning properly.
|
|
253
|
+
const res = await axeFinishRun(driver, axeSource, config, partials, option);
|
|
254
|
+
await driver.close();
|
|
255
|
+
await driver.switchTo().window(win);
|
|
256
|
+
return res;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
exports = module.exports = AxeBuilder;
|
|
261
|
+
|
|
262
|
+
export default AxeBuilder;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WebDriver } from 'selenium-webdriver';
|
|
2
|
+
import type { Spec, AxeResults } from 'axe-core';
|
|
3
|
+
import * as axe from 'axe-core';
|
|
4
|
+
|
|
5
|
+
export interface Options {
|
|
6
|
+
driver: WebDriver;
|
|
7
|
+
axeSource?: string;
|
|
8
|
+
builderOptions?: BuilderOptions;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BuilderOptions {
|
|
12
|
+
noSandbox?: boolean;
|
|
13
|
+
logIframeErrors?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AxeInjectorParams extends Options {
|
|
17
|
+
config?: Spec | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type CallbackFunction = (
|
|
21
|
+
error: string | null,
|
|
22
|
+
results: AxeResults | null
|
|
23
|
+
) => void;
|
|
24
|
+
|
|
25
|
+
export type InjectCallback = (err?: Error) => void;
|
|
26
|
+
|
|
27
|
+
export type PartialResults = Parameters<typeof axe.finishRun>[0];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ContextObject } from 'axe-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get running context
|
|
5
|
+
*/
|
|
6
|
+
export const normalizeContext = (
|
|
7
|
+
include: string[],
|
|
8
|
+
exclude: string[]
|
|
9
|
+
): ContextObject => {
|
|
10
|
+
if (!exclude.length) {
|
|
11
|
+
if (!include.length) {
|
|
12
|
+
return { exclude: [] };
|
|
13
|
+
}
|
|
14
|
+
return { include };
|
|
15
|
+
}
|
|
16
|
+
if (!include.length) {
|
|
17
|
+
return { exclude };
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
include,
|
|
21
|
+
exclude
|
|
22
|
+
};
|
|
23
|
+
};
|