Skip to content

Commit

Permalink
Merge pull request #768 from dequelabs/shadow-exclude
Browse files Browse the repository at this point in the history
Shadow DOM exclude
  • Loading branch information
WilcoFiers authored Mar 12, 2018
2 parents 9af1dea + a902e80 commit 22607b6
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 314 deletions.
5 changes: 4 additions & 1 deletion lib/core/base/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ Audit.prototype.run = function (context, options, resolve, reject) {
'use strict';
this.validateOptions(options);

axe._tree = axe.utils.getFlattenedTree(document.documentElement); //cache the flattened tree
axe._selectCache = [];
var q = axe.utils.queue();
this.rules.forEach(function (rule) {
Expand Down Expand Up @@ -197,6 +196,10 @@ Audit.prototype.after = function (results, options) {

return results.map(function (ruleResult) {
var rule = axe.utils.findBy(rules, 'id', ruleResult.id);
if (!rule) {
// If you see this, you're probably running the Mocha tests with the aXe extension installed
throw new Error('Result for unknown rule. You may be running mismatch aXe-core versions');
}

return rule.after(ruleResult, options);
});
Expand Down
43 changes: 33 additions & 10 deletions lib/core/base/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ function normalizeContext(context) {

// typeof NodeList.length in PhantomJS === function
if (context && typeof context === 'object' || context instanceof NodeList) {

if (context instanceof Node) {
return {
include: [context],
Expand Down Expand Up @@ -130,7 +129,7 @@ function parseSelectorArray(context, type) {
nodeList = Array.from(document.querySelectorAll(item));
//eslint no-loop-func:0
result = result.concat(nodeList.map((node) => {
return axe.utils.getFlattenedTree(node)[0];
return axe.utils.getNodeFromTree(context.flatTree[0], node);
}));
break;
} else if (item && item.length && !(item instanceof Node)) {
Expand All @@ -141,12 +140,15 @@ function parseSelectorArray(context, type) {
nodeList = Array.from(document.querySelectorAll(item[0]));
//eslint no-loop-func:0
result = result.concat(nodeList.map((node) => {
return axe.utils.getFlattenedTree(node)[0];
return axe.utils.getNodeFromTree(context.flatTree[0], node);
}));
}
} else if (item instanceof Node) {

result.push(axe.utils.getFlattenedTree(item)[0]);
if (item.documentElement instanceof Node) {
result.push(context.flatTree[0]);
} else {
result.push(axe.utils.getNodeFromTree(context.flatTree[0], item));
}
}
}

Expand Down Expand Up @@ -180,6 +182,25 @@ function validateContext(context) {
}


/**
* For a context-like object, find its shared root node
*/
function getRootNode ({ include, exclude }) {
const selectors = Array.from(include).concat(Array.from(exclude));
// Find the first Element.ownerDocument or Document
const localDocument = selectors.reduce((result, item) => {
if (result) {
return result;
} else if (item instanceof Element) {
return item.ownerDocument
} else if (item instanceof Document) {
return item
}
}, null);

return (localDocument || document).documentElement;
}

/**
* Holds context of includes, excludes and frames for analysis.
*
Expand All @@ -201,24 +222,26 @@ function validateContext(context) {
* @param {Object} spec Configuration or "specification" object
*/
function Context(spec) {
/* eslint max-statements:["error",19], no-unused-vars:0 */
/* eslint max-statements:["error",22], no-unused-vars:0 */
'use strict';
var self = this;

this.frames = [];
this.initiator = (spec && typeof spec.initiator === 'boolean') ? spec.initiator : true;
this.page = false;

spec = normalizeContext(spec);

//cache the flattened tree
this.flatTree = axe.utils.getFlattenedTree(getRootNode(spec));
this.exclude = spec.exclude;
this.include = spec.include;

this.include = parseSelectorArray(this, 'include');
this.exclude = parseSelectorArray(this, 'exclude');

axe.utils.select('frame, iframe', this).forEach(function (frame) {
if (isNodeInContext(frame, self)) {
pushUniqueFrame(self.frames, frame.actualNode);
axe.utils.select('frame, iframe', this).forEach((frame) => {
if (isNodeInContext(frame, this)) {
pushUniqueFrame(this.frames, frame.actualNode);
}
});

Expand Down
27 changes: 18 additions & 9 deletions lib/core/public/run-rules.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/*global Context */
/*exported runRules */

function complete (cb, ...args) {
cb(...args);
// Clean up after resolve / reject
axe._tree = undefined;
axe._selectorData = undefined;
}

/**
* Starts analysis on the current document and its subframes
* @private
Expand All @@ -12,8 +19,10 @@ function runRules(context, options, resolve, reject) {
'use strict';
try {
context = new Context(context);
axe._tree = context.flatTree;
axe._selectorData = axe.utils.getSelectorData(context.flatTree);
} catch (e) {
return reject(e);
return complete(reject, e);
}

var q = axe.utils.queue();
Expand Down Expand Up @@ -45,10 +54,8 @@ function runRules(context, options, resolve, reject) {
}

// Add wrapper object so that we may use the same "merge" function for results from inside and outside frames
var results = axe.utils.mergeResults(data.map(function (d) {
return {
results: d
};
var results = axe.utils.mergeResults(data.map(function (results) {
return { results };
}));

// after should only run once, so ensure we are in the top level window
Expand All @@ -59,14 +66,16 @@ function runRules(context, options, resolve, reject) {
results = results.map(axe.utils.finalizeRuleResult);
}
try {
resolve(results);
complete(resolve, results);
} catch(e) {
axe.log(e);
complete(axe.log, e);
}
} catch (e) {
reject(e);
complete(reject, e);
}
}).catch(reject);
}).catch((e) => {
complete(reject, e);
});
}

axe._runRules = runRules;
2 changes: 0 additions & 2 deletions lib/core/public/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ axe.run = function (context, options, callback) {
try {
let reporter = axe.getReporter(options.reporter);
let results = reporter(rawResults, options, respond);
axe._selectorData = undefined;
axe._tree = undefined;
if (results !== undefined) {
respond(results);
}
Expand Down
6 changes: 2 additions & 4 deletions lib/core/utils/get-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,9 @@ function getThreeLeastCommonFeatures(elm, selectorData) {
function generateSelector (elm, options, doc) {
/*eslint max-statements:["error", 22], no-loop-func:0*/
if (!axe._selectorData) {
axe._selectorData = axe.utils.getSelectorData(axe._tree);
throw new Error('Expect axe._selectorData to be set up');
}
const {
toRoot = false
} = options;
const { toRoot = false } = options;
let selector;
let similar;

Expand Down
28 changes: 0 additions & 28 deletions test/core/base/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,34 +471,6 @@ describe('Audit', function () {
done();
}, isNotCalled);
});
it('should call axe.utils.getFlattenedTree', function (done) {
var called = false;
axe.utils.getFlattenedTree = function () {
called = true;
};
a.run({ include: [document] }, {
rules: {}
}, function () {
assert.isTrue(called);
axe.utils.getFlattenedTree = getFlattenedTree;
done();
}, isNotCalled);
});
it('should assign the result of getFlattenedTree to axe._tree', function (done) {
var thing = 'honey badger';
var saved = axe.utils.ruleShouldRun;
axe.utils.ruleShouldRun = function () {
assert.equal(axe._tree, thing);
return false;
};
axe.utils.getFlattenedTree = function () {
return thing;
};
a.run({ include: [document] }, {}, function () {
axe.utils.ruleShouldRun = saved;
done();
}, isNotCalled);
});
it('should assign an empty array to axe._selectCache', function (done) {
var saved = axe.utils.ruleShouldRun;
axe.utils.ruleShouldRun = function () {
Expand Down
21 changes: 15 additions & 6 deletions test/core/base/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,12 @@ describe('Context', function() {
});
});

it('should create a flatTree property', function () {
var context = new Context({ include: [document] });
// WARNING: This only works because there is now Shadow DOM on this page
assert.deepEqual(context.flatTree, axe.utils.getFlattenedTree(document));
});

it('should throw when frame could not be found', function (done) {
fixture.innerHTML = '<div id="outer"></div>';
iframeReady('../mock/frames/context.html', $id('outer'), 'target', function() {
Expand All @@ -343,27 +349,29 @@ describe('Context', function() {

describe('object definition', function() {
it('should assign include/exclude', function() {

var flatTree = axe.utils.getFlattenedTree(document);
assert.deepEqual(new Context({
include: ['#fixture'],
exclude: ['#mocha']
}), {
include: [axe.utils.getFlattenedTree(document.getElementById('fixture'))[0]],
exclude: [axe.utils.getFlattenedTree(document.getElementById('mocha'))[0]],
include: axe.utils.querySelectorAll(flatTree, '#fixture'),
exclude: axe.utils.querySelectorAll(flatTree, '#mocha'),
flatTree: flatTree,
initiator: true,
page: false,
frames: []
});

});
it('should disregard bad input, non-matching selectors', function() {

it('should disregard bad input, non-matching selectors', function() {
var flatTree = axe.utils.getFlattenedTree(document);
assert.deepEqual(new Context({
include: ['#fixture', '#monkeys'],
exclude: ['#bananas']
}), {
include: [axe.utils.getFlattenedTree(document.getElementById('fixture'))[0]],
include: axe.utils.querySelectorAll(flatTree, '#fixture'),
exclude: [],
flatTree: flatTree,
initiator: true,
page: false,
frames: []
Expand Down Expand Up @@ -429,6 +437,7 @@ describe('Context', function() {
it('should not throw given really weird circumstances when hasOwnProperty is deleted from a document node?', function() {
var spec = document.implementation.createHTMLDocument('ie is dumb');
spec.hasOwnProperty = undefined;

var result = new Context(spec);

assert.lengthOf(result.include, 1);
Expand Down
52 changes: 48 additions & 4 deletions test/core/public/run-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('runRules', function () {
afterEach(function () {
fixture.innerHTML = '';
axe._audit = null;
axe._tree = undefined;
});

it('should work', function (done) {
Expand All @@ -72,7 +73,6 @@ describe('runRules', function () {

frame.addEventListener('load', function () {
setTimeout(function () {
axe._tree = axe.utils.getFlattenedTree(document);
runRules(document, {}, function (r) {
assert.lengthOf(r[0].passes, 3);
done();
Expand All @@ -97,7 +97,6 @@ describe('runRules', function () {
var frame = document.createElement('iframe');
frame.addEventListener('load', function () {
setTimeout(function () {
axe._tree = axe.utils.getFlattenedTree(document);
runRules(document, {}, function (r) {
var nodes = r[0].passes.map(function (detail) {
return detail.node.selector;
Expand Down Expand Up @@ -156,7 +155,6 @@ describe('runRules', function () {
var div = document.createElement('div');
fixture.appendChild(div);

axe._tree = axe.utils.getFlattenedTree(document);
runRules('#fixture', {}, function (results) {
assert.deepEqual(JSON.parse(JSON.stringify(results)), [{
id: 'div#target',
Expand Down Expand Up @@ -232,7 +230,6 @@ describe('runRules', function () {
});

iframeReady('../mock/frames/context.html', fixture, 'context-test', function () {
axe._tree = axe.utils.getFlattenedTree(document);
runRules('#not-happening', {}, function () {
assert.fail('This selector should not exist.');
}, function (error) {
Expand Down Expand Up @@ -653,4 +650,51 @@ describe('runRules', function () {
done();
}, isNotCalled);
});

it('should clear up axe._tree / axe._selectorData after resolving', function (done) {
axe._load({ rules: [{
id: 'html',
selector: 'html',
any: ['html']
}], checks: [{
id: 'html',
evaluate: function () {
return true;
}
}], messages: {}});

runRules(document, {}, function resolve() {
assert.isDefined(axe._tree);
assert.isDefined(axe._selectorData);
setTimeout(function () {
assert.isUndefined(axe._tree);
assert.isUndefined(axe._selectorData);
done();
}, 10);
}, isNotCalled);
});

it('should clear up axe._tree / axe._selectorData after an error', function (done) {
axe._load({ rules: [{
id: 'invalidRule'
}], checks: [], messages: {}});

createFrames(function () {
setTimeout(function () {
runRules(document, {}, function () {
assert.ok(false, 'You shall not pass!');
done();
},
function () {
assert.isDefined(axe._tree);
assert.isDefined(axe._selectorData);
setTimeout(function () {
assert.isUndefined(axe._tree);
assert.isUndefined(axe._selectorData);
done();
}, 10);
});
}, 100);
});
});
});
Loading

0 comments on commit 22607b6

Please sign in to comment.