Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(run-virtual-rule): new api to run rules using only virtual nodes #1594

Merged
merged 13 commits into from
Jun 10, 2019
4 changes: 3 additions & 1 deletion lib/core/base/rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ Rule.prototype.runSync = function(context, options = {}) {

const result = getResult(results);
if (result) {
result.node = new axe.utils.DqElement(node.actualNode, options);
result.node = node.actualNode
? new axe.utils.DqElement(node.actualNode, options)
: node;
straker marked this conversation as resolved.
Show resolved Hide resolved
ruleResult.nodes.push(result);
}
});
Expand Down
9 changes: 5 additions & 4 deletions lib/core/base/virtual-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ const whitespaceRegex = /[\t\r\n\f]/g;
class VirtualNode {
/**
* Wrap the real node and provide list of the flattened children
*
* @param node {Node} the node in question
* @param shadowId {String} the ID of the shadow DOM to which this node belongs
* @param {Node} node the node in question
* @param {VirtualNode} parent The parent VirtualNode
* @param {String} shadowId the ID of the shadow DOM to which this node belongs
*/
constructor(node, shadowId) {
constructor(node, parent, shadowId) {
straker marked this conversation as resolved.
Show resolved Hide resolved
this.shadowId = shadowId;
this.children = [];
this.actualNode = node;
this.parent = parent;

this._isHidden = null; // will be populated by axe.utils.isHidden
this._cache = {};
Expand Down
46 changes: 46 additions & 0 deletions lib/core/public/run-virtual-rule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* global helpers */

/**
* Run a rule in a non-browser environment
* @param {String} ruleId Id of the rule
* @param {VirtualNode} vNode The virtual node to run the rule against
* @param {Object} options (optional) Set of options passed into rules or checks
* @return {Object} axe results for the rule run
*/
axe.runVirtualRule = function(ruleId, vNode, options = {}) {
options.reporter = options.reporter || axe._audit.reporter || 'v1';
axe._selectorData = {};

let rule = axe._audit.rules.find(rule => rule.id === ruleId);

if (!rule) {
throw new Error('unknown rule `' + ruleId + '`');
}

// rule.prototype.gather calls axe.utils.isHidden which in turn calls
// window.getComputedStyle if the rule excludes hidden elements. we
// can avoid this call by forcing the rule to not exclude hidden
// elements
rule = Object.create(rule, { excludeHidden: { value: false } });

const context = {
include: [vNode]
};

const rawResults = rule.runSync(context, options);
axe.utils.publishMetaData(rawResults);
axe.utils.finalizeRuleResult(rawResults);
const results = axe.utils.aggregateResult([rawResults]);

results.violations.forEach(result =>
result.nodes.forEach(nodeResult => {
nodeResult.failureSummary = helpers.failureSummary(nodeResult);
})
);

return {
...helpers.getEnvironmentData(),
...results,
toolOptions: options
};
};
40 changes: 27 additions & 13 deletions lib/core/utils/contains.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,43 @@
* Wrapper for Node#contains; PhantomJS does not support Node#contains and erroneously reports that it does
* @method contains
* @memberof axe.utils
* @param {HTMLElement} node The candidate container node
* @param {HTMLElement} otherNode The node to test is contained by `node`
* @return {Boolean} Whether `node` contains `otherNode`
* @param {VirtualNode} vNode The candidate container VirtualNode
* @param {VirtualNode} otherVNode The vNode to test is contained by `vNode`
* @return {Boolean} Whether `vNode` contains `otherVNode`
*/
axe.utils.contains = function(node, otherNode) {
axe.utils.contains = function(vNode, otherVNode) {
/*eslint no-bitwise: 0*/
'use strict';
function containsShadowChild(node, otherNode) {
if (node.shadowId === otherNode.shadowId) {
function containsShadowChild(vNode, otherVNode) {
if (vNode.shadowId === otherVNode.shadowId) {
return true;
}
return !!node.children.find(child => {
return containsShadowChild(child, otherNode);
return !!vNode.children.find(child => {
return containsShadowChild(child, otherVNode);
});
}

if (node.shadowId || otherNode.shadowId) {
return containsShadowChild(node, otherNode);
if (vNode.shadowId || otherVNode.shadowId) {
return containsShadowChild(vNode, otherVNode);
}

if (typeof node.actualNode.contains === 'function') {
return node.actualNode.contains(otherNode.actualNode);
if (vNode.actualNode) {
if (typeof vNode.actualNode.contains === 'function') {
return vNode.actualNode.contains(otherVNode.actualNode);
}

return !!(
vNode.actualNode.compareDocumentPosition(otherVNode.actualNode) & 16
);
} else {
// fallback for virtualNode only contexts (e.g. linting)
// @see https://github.com/Financial-Times/polyfill-service/pull/183/files
do {
if (otherVNode === vNode) {
return true;
}
} while ((otherVNode = otherVNode && otherVNode.parent));
}

return !!(node.actualNode.compareDocumentPosition(otherNode.actualNode) & 16);
return false;
};
43 changes: 27 additions & 16 deletions lib/core/utils/flattened-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ function getSlotChildren(node) {
* @param {Node} node the current node
* @param {String} shadowId, optional ID of the shadow DOM that is the closest shadow
* ancestor of the node
* @param {VirtualNode} parent the parent VirtualNode
*/
function flattenTree(node, shadowId) {
function flattenTree(node, shadowId, parent) {
// using a closure here and therefore cannot easily refactor toreduce the statements
var retVal, realArray, nodeName;
function reduceShadowDOM(res, child) {
var replacements = flattenTree(child, shadowId);
function reduceShadowDOM(res, child, parent) {
var replacements = flattenTree(child, shadowId, parent);
if (replacements) {
res = res.concat(replacements);
}
Expand All @@ -66,22 +67,24 @@ function flattenTree(node, shadowId) {
if (axe.utils.isShadowRoot(node)) {
// generate an ID for this shadow root and overwrite the current
// closure shadowId with this value so that it cascades down the tree
retVal = new VirtualNode(node, shadowId);
retVal = new VirtualNode(node, parent, shadowId);
shadowId =
'a' +
Math.random()
.toString()
.substring(2);
realArray = Array.from(node.shadowRoot.childNodes);
retVal.children = realArray.reduce(reduceShadowDOM, []);
retVal.children = realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, retVal);
}, []);

return [retVal];
} else {
if (
nodeName === 'content' &&
typeof node.getDistributedNodes === 'function'
) {
if (nodeName === 'content') {
realArray = Array.from(node.getDistributedNodes());
return realArray.reduce(reduceShadowDOM, []);
return realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, parent);
}, []);
} else if (
nodeName === 'slot' &&
typeof node.assignedNodes === 'function'
Expand All @@ -96,21 +99,29 @@ function flattenTree(node, shadowId) {
if (false && styl.display !== 'contents') {
// intentionally commented out
// has a box
retVal = new VirtualNode(node, shadowId);
retVal.children = realArray.reduce(reduceShadowDOM, []);
retVal = new VirtualNode(node, parent, shadowId);
retVal.children = realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, retVal);
}, []);

return [retVal];
} else {
return realArray.reduce(reduceShadowDOM, []);
return realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, parent);
}, []);
}
} else {
if (node.nodeType === 1) {
retVal = new VirtualNode(node, shadowId);
retVal = new VirtualNode(node, parent, shadowId);
realArray = Array.from(node.childNodes);
retVal.children = realArray.reduce(reduceShadowDOM, []);
retVal.children = realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, retVal);
}, []);

return [retVal];
} else if (node.nodeType === 3) {
// text
return [new VirtualNode(node)];
return [new VirtualNode(node, parent)];
}
return undefined;
}
Expand Down
34 changes: 12 additions & 22 deletions lib/core/utils/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ function getDeepest(collection) {
function isNodeInContext(node, context) {
'use strict';

var include =
const include =
context.include &&
getDeepest(
context.include.filter(function(candidate) {
return axe.utils.contains(candidate, node);
})
);
var exclude =
const exclude =
context.exclude &&
getDeepest(
context.exclude.filter(function(candidate) {
Expand All @@ -58,7 +58,7 @@ function isNodeInContext(node, context) {
function pushNode(result, nodes) {
'use strict';

var temp;
let temp;

if (result.length === 0) {
return nodes;
Expand All @@ -69,7 +69,7 @@ function pushNode(result, nodes) {
result = nodes;
nodes = temp;
}
for (var i = 0, l = nodes.length; i < l; i++) {
for (let i = 0, l = nodes.length; i < l; i++) {
if (!result.includes(nodes[i])) {
result.push(nodes[i]);
}
Expand All @@ -84,10 +84,7 @@ function pushNode(result, nodes) {
*/
function reduceIncludes(includes) {
return includes.reduce((res, el) => {
if (
!res.length ||
!res[res.length - 1].actualNode.contains(el.actualNode)
) {
if (!res.length || !axe.utils.contains(res[res.length - 1], el)) {
res.push(el);
}
return res;
Expand All @@ -103,33 +100,26 @@ function reduceIncludes(includes) {
axe.utils.select = function select(selector, context) {
'use strict';

var result = [],
candidate;
let result = [];
let candidate;
if (axe._selectCache) {
// if used outside of run, it will still work
for (var j = 0, l = axe._selectCache.length; j < l; j++) {
for (let j = 0, l = axe._selectCache.length; j < l; j++) {
// First see whether the item exists in the cache
let item = axe._selectCache[j];
const item = axe._selectCache[j];
if (item.selector === selector) {
return item.result;
}
}
}
var curried = (function(context) {
const curried = (function(context) {
return function(node) {
return isNodeInContext(node, context);
};
})(context);
var reducedIncludes = reduceIncludes(context.include);
for (var i = 0; i < reducedIncludes.length; i++) {
const reducedIncludes = reduceIncludes(context.include);
for (let i = 0; i < reducedIncludes.length; i++) {
candidate = reducedIncludes[i];
if (
candidate.actualNode.nodeType === candidate.actualNode.ELEMENT_NODE &&
axe.utils.matchesSelector(candidate.actualNode, selector) &&
curried(candidate)
) {
result = pushNode(result, [candidate]);
WilcoFiers marked this conversation as resolved.
Show resolved Hide resolved
}
result = pushNode(
result,
axe.utils.querySelectorAllFilter(candidate, selector, curried)
Expand Down
8 changes: 5 additions & 3 deletions test/core/base/virtual-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ describe('VirtualNode', function() {
assert.isFunction(VirtualNode);
});

it('should accept two parameters', function() {
assert.lengthOf(VirtualNode, 2);
it('should accept three parameters', function() {
assert.lengthOf(VirtualNode, 3);
});

describe('prototype', function() {
it('should have public properties', function() {
var vNode = new VirtualNode(node, 'foo');
var parent = {};
var vNode = new VirtualNode(node, parent, 'foo');

assert.equal(vNode.shadowId, 'foo');
assert.typeOf(vNode.children, 'array');
assert.equal(vNode.actualNode, node);
assert.equal(vNode.parent, parent);
});

it('should abstract Node properties', function() {
Expand Down
Loading