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

Commit

Permalink
fix: Prevent infinitely recursing when injecting into iframes (#66)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stephenmathieson authored Jun 22, 2018
1 parent 1586dbd commit 591a701
Show file tree
Hide file tree
Showing 11 changed files with 3,115 additions and 901 deletions.
17 changes: 17 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"presets": [
["env", {
"targets": {
"node": "4"
}
}]
],
"plugins": [
["transform-runtime", {
"helpers": false,
"polyfill": false,
"regenerator": true,
"moduleName": "babel-runtime"
}]
]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ output
npm-shrinkwrap.json
.nyc_output/
coverage/
dist
9 changes: 2 additions & 7 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
node_modules
output
test
Gruntfile.js
typings
.editorconfig
.travis.yml
.gitignore
CONTRIBUTING.md
.nyc_output/
coverage/
82 changes: 82 additions & 0 deletions lib/axe-injector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
class AxeInjector {
constructor({ driver, config, axeSource = null }) {
this.driver = driver;
this.axeSource = axeSource || require('axe-core').source;
this.config = config ? JSON.stringify(config) : '';

this.didLogError = false;
this.errorHandler = this.errorHandler.bind(this);
}

// Single-shot error handler. Ensures we don't log more than once.
errorHandler() {
// We've already "warned" the user. No need to do it again (mostly for backwards compatiability)
if (this.didLogError) {
return;
}

this.didLogError = true;
// eslint-disable-next-line no-console
console.log('Failed to inject axe-core into one of the iframes!');
}

// Get axe-core source (and configuration)
get script() {
return `
${this.axeSource}
${this.config ? `axe.configure(${this.config})` : ''}
axe.configure({ branding: { application: 'webdriverjs' } })
`;
}

// Inject into the provided `frame` and its child `frames`
async handleFrame(frame) {
// Switch context to the frame and inject our `script` into it
await this.driver.switchTo().frame(frame);
await this.driver.executeScript(this.script);

// Get all of <iframe>s at this level
const frames = await this.driver.findElements({ tagName: 'iframe' });

// Inject into each frame. Handling errors to ensure an issue on a single frame won't stop the rest of the injections.
return Promise.all(
frames.map(childFrame =>
this.handleFrame(childFrame).catch(this.errorHandler)
)
);
}

// Inject into all frames.
async injectIntoAllFrames() {
// Ensure we're "starting" our loop at the top-most frame
await this.driver.switchTo().defaultContent();

// Inject the script into the top-level
// XXX: if this `executeScript` fails, we *want* to error, as we cannot run axe-core.
await this.driver.executeScript(this.script);

// Get all of <iframe>s at this level
const frames = await this.driver.findElements({ tagName: 'iframe' });

// Inject the script into all child frames. Handle errors to ensure we don't stop execution if we fail to inject.
await Promise.all(
frames.map(childFrame =>
this.handleFrame(childFrame).catch(this.errorHandler)
)
);

// Move back to the top-most frame
return this.driver.switchTo().defaultContent();
}

// Inject axe, invoking the provided callback when done
inject(callback) {
this.injectIntoAllFrames()
.then(() => callback())
// For now, we intentionally ignore errors here, as
// allowing them to bubble up would be a breaking change.
.catch(() => callback());
}
}

module.exports = AxeInjector;
7 changes: 4 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
var inject = require('./inject'),
normalizeContext = require('./normalize-context');
var AxeInjector = require('./axe-injector');
var normalizeContext = require('./normalize-context');

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

return new Promise(function(resolve) {
inject(driver, source, config, function() {
var injector = new AxeInjector({ driver, axeSource: source, config });
injector.inject(() => {
driver
.executeAsyncScript(
function(context, options, config) {
Expand Down
67 changes: 0 additions & 67 deletions lib/inject.js

This file was deleted.

Loading

0 comments on commit 591a701

Please sign in to comment.