Skip to content
This repository was archived by the owner on Mar 19, 2021. It is now read-only.

Commit 591a701

Browse files
fix: Prevent infinitely recursing when injecting into iframes (#66)
This patch prevents `axe-webdriverjs` from infinitely recursing when injecting `axe-core` into `<iframe>`s. I've re-written the code we use for injecting which will only recurse as deep as the number of nested `<iframe>`s on the page. In order to keep the code "easy to read/write", I've used `async/await` rather than chaining `Promise`s together. Because we do not have a clear picture of what Node.js versions we need to support here, I've added Babel in order to ensure `async/await` works in older Nodes. It is currently setup use support Node v4. Closes #63.
1 parent 1586dbd commit 591a701

File tree

11 files changed

+3115
-901
lines changed

11 files changed

+3115
-901
lines changed

.babelrc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"presets": [
3+
["env", {
4+
"targets": {
5+
"node": "4"
6+
}
7+
}]
8+
],
9+
"plugins": [
10+
["transform-runtime", {
11+
"helpers": false,
12+
"polyfill": false,
13+
"regenerator": true,
14+
"moduleName": "babel-runtime"
15+
}]
16+
]
17+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ output
44
npm-shrinkwrap.json
55
.nyc_output/
66
coverage/
7+
dist

.npmignore

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
node_modules
22
output
3-
test
4-
Gruntfile.js
5-
typings
6-
.editorconfig
7-
.travis.yml
8-
.gitignore
9-
CONTRIBUTING.md
3+
.nyc_output/
4+
coverage/

lib/axe-injector.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
class AxeInjector {
2+
constructor({ driver, config, axeSource = null }) {
3+
this.driver = driver;
4+
this.axeSource = axeSource || require('axe-core').source;
5+
this.config = config ? JSON.stringify(config) : '';
6+
7+
this.didLogError = false;
8+
this.errorHandler = this.errorHandler.bind(this);
9+
}
10+
11+
// Single-shot error handler. Ensures we don't log more than once.
12+
errorHandler() {
13+
// We've already "warned" the user. No need to do it again (mostly for backwards compatiability)
14+
if (this.didLogError) {
15+
return;
16+
}
17+
18+
this.didLogError = true;
19+
// eslint-disable-next-line no-console
20+
console.log('Failed to inject axe-core into one of the iframes!');
21+
}
22+
23+
// Get axe-core source (and configuration)
24+
get script() {
25+
return `
26+
${this.axeSource}
27+
${this.config ? `axe.configure(${this.config})` : ''}
28+
axe.configure({ branding: { application: 'webdriverjs' } })
29+
`;
30+
}
31+
32+
// Inject into the provided `frame` and its child `frames`
33+
async handleFrame(frame) {
34+
// Switch context to the frame and inject our `script` into it
35+
await this.driver.switchTo().frame(frame);
36+
await this.driver.executeScript(this.script);
37+
38+
// Get all of <iframe>s at this level
39+
const frames = await this.driver.findElements({ tagName: 'iframe' });
40+
41+
// Inject into each frame. Handling errors to ensure an issue on a single frame won't stop the rest of the injections.
42+
return Promise.all(
43+
frames.map(childFrame =>
44+
this.handleFrame(childFrame).catch(this.errorHandler)
45+
)
46+
);
47+
}
48+
49+
// Inject into all frames.
50+
async injectIntoAllFrames() {
51+
// Ensure we're "starting" our loop at the top-most frame
52+
await this.driver.switchTo().defaultContent();
53+
54+
// Inject the script into the top-level
55+
// XXX: if this `executeScript` fails, we *want* to error, as we cannot run axe-core.
56+
await this.driver.executeScript(this.script);
57+
58+
// Get all of <iframe>s at this level
59+
const frames = await this.driver.findElements({ tagName: 'iframe' });
60+
61+
// Inject the script into all child frames. Handle errors to ensure we don't stop execution if we fail to inject.
62+
await Promise.all(
63+
frames.map(childFrame =>
64+
this.handleFrame(childFrame).catch(this.errorHandler)
65+
)
66+
);
67+
68+
// Move back to the top-most frame
69+
return this.driver.switchTo().defaultContent();
70+
}
71+
72+
// Inject axe, invoking the provided callback when done
73+
inject(callback) {
74+
this.injectIntoAllFrames()
75+
.then(() => callback())
76+
// For now, we intentionally ignore errors here, as
77+
// allowing them to bubble up would be a breaking change.
78+
.catch(() => callback());
79+
}
80+
}
81+
82+
module.exports = AxeInjector;

lib/index.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
var inject = require('./inject'),
2-
normalizeContext = require('./normalize-context');
1+
var AxeInjector = require('./axe-injector');
2+
var normalizeContext = require('./normalize-context');
33

44
/**
55
* Constructor for chainable WebDriver API
@@ -129,7 +129,8 @@ AxeBuilder.prototype.analyze = function(callback) {
129129
source = this._source;
130130

131131
return new Promise(function(resolve) {
132-
inject(driver, source, config, function() {
132+
var injector = new AxeInjector({ driver, axeSource: source, config });
133+
injector.inject(() => {
133134
driver
134135
.executeAsyncScript(
135136
function(context, options, config) {

lib/inject.js

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)