@axe-core/webdriverjs 4.3.3-alpha.242 → 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/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
+ };