@axe-core/playwright 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/README.md +2 -24
- package/dist/AxePartialRunner.js +7 -11
- package/dist/AxePartialRunner.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +40 -18
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/error-handling.md +43 -0
- package/example.js +12 -0
- package/package.json +7 -10
- package/src/AxePartialRunner.ts +51 -0
- package/src/browser.ts +43 -0
- package/src/index.ts +308 -0
- package/src/types.ts +43 -0
- package/src/utils.ts +50 -0
- package/tests/axe-playwright.spec.ts +664 -0
- package/tests/fixtures/context.html +11 -0
- package/tsconfig.json +15 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as assert from 'assert';
|
|
3
|
+
import type { Page, Frame, ElementHandle } from 'playwright';
|
|
4
|
+
import type {
|
|
5
|
+
RunOptions,
|
|
6
|
+
AxeResults,
|
|
7
|
+
ContextObject,
|
|
8
|
+
PartialResults
|
|
9
|
+
} from 'axe-core';
|
|
10
|
+
import { normalizeContext, analyzePage } from './utils';
|
|
11
|
+
import type { AxePlaywrightParams } from './types';
|
|
12
|
+
import {
|
|
13
|
+
axeFinishRun,
|
|
14
|
+
axeGetFrameContexts,
|
|
15
|
+
axeRunPartial,
|
|
16
|
+
axeShadowSelect
|
|
17
|
+
} from './browser';
|
|
18
|
+
import AxePartialRunner from './AxePartialRunner';
|
|
19
|
+
|
|
20
|
+
export default class AxeBuilder {
|
|
21
|
+
private page: Page;
|
|
22
|
+
private includes: string[];
|
|
23
|
+
private excludes: string[];
|
|
24
|
+
private option: RunOptions;
|
|
25
|
+
private source: string;
|
|
26
|
+
private legacyMode = false;
|
|
27
|
+
|
|
28
|
+
constructor({ page, axeSource }: AxePlaywrightParams) {
|
|
29
|
+
const axePath = require.resolve('axe-core');
|
|
30
|
+
const source = fs.readFileSync(axePath, 'utf-8');
|
|
31
|
+
this.page = page;
|
|
32
|
+
this.includes = [];
|
|
33
|
+
this.excludes = [];
|
|
34
|
+
this.option = {};
|
|
35
|
+
this.source = axeSource || source;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Selector to include in analysis.
|
|
40
|
+
* This may be called any number of times.
|
|
41
|
+
* @param String selector
|
|
42
|
+
* @returns this
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
public include(selector: string): this {
|
|
46
|
+
this.includes.push(selector);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Selector to exclude in analysis.
|
|
52
|
+
* This may be called any number of times.
|
|
53
|
+
* @param String selector
|
|
54
|
+
* @returns this
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
public exclude(selector: string): this {
|
|
58
|
+
this.excludes.push(selector);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Set options to be passed into axe-core
|
|
64
|
+
* @param RunOptions options
|
|
65
|
+
* @returns AxeBuilder
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
public options(options: RunOptions): this {
|
|
69
|
+
this.option = options;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Limit analysis to only the specified rules.
|
|
75
|
+
* Cannot be used with `AxeBuilder#withTags`
|
|
76
|
+
* @param String|Array rules
|
|
77
|
+
* @returns this
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
public withRules(rules: string | string[]): this {
|
|
81
|
+
rules = Array.isArray(rules) ? rules : [rules];
|
|
82
|
+
/* istanbul ignore next */
|
|
83
|
+
this.option = this.option || {};
|
|
84
|
+
this.option.runOnly = {
|
|
85
|
+
type: 'rule',
|
|
86
|
+
values: rules
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Limit analysis to only specified tags.
|
|
94
|
+
* Cannot be used with `AxeBuilder#withRules`
|
|
95
|
+
* @param String|Array tags
|
|
96
|
+
* @returns this
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
public withTags(tags: string | string[]): this {
|
|
100
|
+
tags = Array.isArray(tags) ? tags : [tags];
|
|
101
|
+
/* istanbul ignore next */
|
|
102
|
+
this.option = this.option || {};
|
|
103
|
+
this.option.runOnly = {
|
|
104
|
+
type: 'tag',
|
|
105
|
+
values: tags
|
|
106
|
+
};
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Set the list of rules to skip when running an analysis.
|
|
112
|
+
* @param String|Array rules
|
|
113
|
+
* @returns this
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
public disableRules(rules: string | string[]): this {
|
|
117
|
+
rules = Array.isArray(rules) ? rules : [rules];
|
|
118
|
+
/* istanbul ignore next */
|
|
119
|
+
this.option = this.option || {};
|
|
120
|
+
this.option.rules = {};
|
|
121
|
+
|
|
122
|
+
for (const rule of rules) {
|
|
123
|
+
this.option.rules[rule] = { enabled: false };
|
|
124
|
+
}
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Use frameMessenger with <same_origin_only>
|
|
130
|
+
*
|
|
131
|
+
* This disables use of axe.runPartial() which is called in each frame, and
|
|
132
|
+
* axe.finishRun() which is called in a blank page. This uses axe.run() instead,
|
|
133
|
+
* but with the restriction that cross-origin frames will not be tested.
|
|
134
|
+
*/
|
|
135
|
+
public setLegacyMode(legacyMode = true): this {
|
|
136
|
+
this.legacyMode = legacyMode;
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Perform analysis and retrieve results. *Does not chain.*
|
|
142
|
+
* @return Promise<Result | Error>
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
public async analyze(): Promise<AxeResults> {
|
|
146
|
+
const context = normalizeContext(this.includes, this.excludes);
|
|
147
|
+
const { page, option: options } = this;
|
|
148
|
+
|
|
149
|
+
page.evaluate(this.script());
|
|
150
|
+
const runPartialDefined = await page.evaluate<boolean>(
|
|
151
|
+
'typeof window.axe.runPartial === "function"'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
let results: AxeResults;
|
|
155
|
+
|
|
156
|
+
if (!runPartialDefined || this.legacyMode) {
|
|
157
|
+
results = await this.runLegacy(context);
|
|
158
|
+
return results;
|
|
159
|
+
}
|
|
160
|
+
const partialResults = await this.runPartialRecursive(
|
|
161
|
+
page.mainFrame(),
|
|
162
|
+
context
|
|
163
|
+
);
|
|
164
|
+
const partials = await partialResults.getPartials();
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
return await this.finishRun(partials);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`${error.message}\n Please check out https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/error-handling.md`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Injects `axe-core` into all frames.
|
|
177
|
+
* @param Page - playwright page object
|
|
178
|
+
* @returns Promise<void>
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
private async inject(frames: Frame[]): Promise<void> {
|
|
182
|
+
for (const iframe of frames) {
|
|
183
|
+
await iframe.evaluate(this.script());
|
|
184
|
+
await iframe.evaluate(await this.axeConfigure());
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get axe-core source and configurations
|
|
190
|
+
* @returns String
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
private script(): string {
|
|
194
|
+
return this.source;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private async runLegacy(context: ContextObject): Promise<AxeResults> {
|
|
198
|
+
// in playwright all frames are available in `.frames()`, even nested and
|
|
199
|
+
// shadowDOM iframes. also navigating to a url causes it to be put into
|
|
200
|
+
// an iframe so we don't need to inject into the page object itself
|
|
201
|
+
const frames = this.page.frames();
|
|
202
|
+
await this.inject(frames);
|
|
203
|
+
const axeResults = await this.page.evaluate(analyzePage, {
|
|
204
|
+
context,
|
|
205
|
+
options: this.option
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (axeResults.error) {
|
|
209
|
+
throw new Error(axeResults.error);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return axeResults.results;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Inject `axe-core` into each frame and run `axe.runPartial`.
|
|
217
|
+
* Because we need to inject axe into all frames all at once
|
|
218
|
+
* (to avoid any potential problems with the DOM becoming out-of-sync)
|
|
219
|
+
* but also need to not process results for any child frames if the parent
|
|
220
|
+
* frame throws an error (requirements of the data structure for `axe.finishRun`),
|
|
221
|
+
* we have to return a deeply nested array of Promises and then flatten
|
|
222
|
+
* the array once all Promises have finished, throwing out any nested Promises
|
|
223
|
+
* if the parent Promise is not fulfilled.
|
|
224
|
+
* @param frame - playwright frame object
|
|
225
|
+
* @param context - axe-core context object
|
|
226
|
+
* @returns Promise<AxePartialRunner>
|
|
227
|
+
*/
|
|
228
|
+
|
|
229
|
+
private async runPartialRecursive(
|
|
230
|
+
frame: Frame,
|
|
231
|
+
context: ContextObject
|
|
232
|
+
): Promise<AxePartialRunner> {
|
|
233
|
+
const frameContexts = await frame.evaluate(axeGetFrameContexts, {
|
|
234
|
+
context
|
|
235
|
+
});
|
|
236
|
+
const partialPromise = frame.evaluate(axeRunPartial, {
|
|
237
|
+
context,
|
|
238
|
+
options: this.option
|
|
239
|
+
});
|
|
240
|
+
const initiator = frame === this.page.mainFrame();
|
|
241
|
+
const axePartialRunner = new AxePartialRunner(partialPromise, initiator);
|
|
242
|
+
|
|
243
|
+
for (const { frameSelector, frameContext } of frameContexts) {
|
|
244
|
+
let childResults: AxePartialRunner | null = null;
|
|
245
|
+
try {
|
|
246
|
+
const iframeHandle = await frame.evaluateHandle(axeShadowSelect, {
|
|
247
|
+
frameSelector
|
|
248
|
+
});
|
|
249
|
+
// note: these can return null but the catch will handle this properly for all cases
|
|
250
|
+
const iframeElement =
|
|
251
|
+
iframeHandle.asElement() as ElementHandle<Element>;
|
|
252
|
+
const childFrame = await iframeElement.contentFrame();
|
|
253
|
+
if (childFrame) {
|
|
254
|
+
await this.inject([childFrame]);
|
|
255
|
+
childResults = await this.runPartialRecursive(
|
|
256
|
+
childFrame,
|
|
257
|
+
frameContext
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
/* do nothing */
|
|
262
|
+
}
|
|
263
|
+
axePartialRunner.addChildResults(childResults);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return axePartialRunner;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private async finishRun(partialResults: PartialResults): Promise<AxeResults> {
|
|
270
|
+
const { page, option: options } = this;
|
|
271
|
+
const context = page.context();
|
|
272
|
+
const blankPage = await context.newPage();
|
|
273
|
+
|
|
274
|
+
assert(
|
|
275
|
+
blankPage,
|
|
276
|
+
'Please make sure that you have popup blockers disabled.'
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
blankPage.evaluate(this.script());
|
|
280
|
+
blankPage.evaluate(await this.axeConfigure());
|
|
281
|
+
|
|
282
|
+
return await blankPage
|
|
283
|
+
.evaluate(axeFinishRun, {
|
|
284
|
+
partialResults,
|
|
285
|
+
options
|
|
286
|
+
})
|
|
287
|
+
.finally(() => {
|
|
288
|
+
blankPage.close();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async axeConfigure(): Promise<string> {
|
|
293
|
+
const hasRunPartial = await this.page.evaluate<boolean>(
|
|
294
|
+
'typeof window.axe?.runPartial === "function"'
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return `
|
|
298
|
+
;axe.configure({
|
|
299
|
+
${
|
|
300
|
+
!this.legacyMode && !hasRunPartial
|
|
301
|
+
? 'allowedOrigins: ["<unsafe_all_origins>"],'
|
|
302
|
+
: 'allowedOrigins: ["<same_origin>"],'
|
|
303
|
+
}
|
|
304
|
+
branding: { application: 'playwright' }
|
|
305
|
+
})
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
import type {
|
|
3
|
+
RunOptions,
|
|
4
|
+
AxeResults,
|
|
5
|
+
ContextObject,
|
|
6
|
+
CrossTreeSelector,
|
|
7
|
+
PartialResult
|
|
8
|
+
} from 'axe-core';
|
|
9
|
+
|
|
10
|
+
export type PartialResults = PartialResult | null;
|
|
11
|
+
|
|
12
|
+
export interface AnalyzePageParams {
|
|
13
|
+
context: ContextObject;
|
|
14
|
+
options: RunOptions | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AxePlaywrightParams {
|
|
18
|
+
page: Page;
|
|
19
|
+
axeSource?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AnalyzePageResponse {
|
|
23
|
+
results: AxeResults;
|
|
24
|
+
error: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GetFrameContextsParams {
|
|
28
|
+
context: ContextObject;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ShadowSelectParams {
|
|
32
|
+
frameSelector: CrossTreeSelector;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RunPartialParams {
|
|
36
|
+
context: ContextObject;
|
|
37
|
+
options: RunOptions;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FinishRunParams {
|
|
41
|
+
partialResults: PartialResults[];
|
|
42
|
+
options: RunOptions;
|
|
43
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AxeResults, ContextObject } from 'axe-core';
|
|
2
|
+
import type { AnalyzePageParams, AnalyzePageResponse } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get running context
|
|
6
|
+
* @param Array include
|
|
7
|
+
* @param Array exclude
|
|
8
|
+
* @returns ContextObject
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const normalizeContext = (
|
|
12
|
+
includes: string[],
|
|
13
|
+
excludes: string[]
|
|
14
|
+
): ContextObject => {
|
|
15
|
+
const base: ContextObject = {
|
|
16
|
+
exclude: []
|
|
17
|
+
};
|
|
18
|
+
if (excludes.length && Array.isArray(base.exclude)) {
|
|
19
|
+
base.exclude.push(...excludes);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (includes.length) {
|
|
23
|
+
base.include = includes;
|
|
24
|
+
}
|
|
25
|
+
return base;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Analyze the page.
|
|
30
|
+
* @param AnalyzePageParams analyzeContext
|
|
31
|
+
* @returns Promise<AnalyzePageResponse>
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export const analyzePage = ({
|
|
35
|
+
context,
|
|
36
|
+
options
|
|
37
|
+
}: AnalyzePageParams): Promise<AnalyzePageResponse> => {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const axeCore = (window as any).axe;
|
|
40
|
+
|
|
41
|
+
// Run axe-core
|
|
42
|
+
return axeCore
|
|
43
|
+
.run(context || document, options || {})
|
|
44
|
+
.then((results: AxeResults) => {
|
|
45
|
+
return { error: null, results };
|
|
46
|
+
})
|
|
47
|
+
.catch((err: Error) => {
|
|
48
|
+
return { error: err.message, results: null };
|
|
49
|
+
});
|
|
50
|
+
};
|