Skip to content

Commit

Permalink
feat(run-virtual-rule): new api to run rules using only virtual nodes (
Browse files Browse the repository at this point in the history
…#1594)

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

* do not modify original rule

* fix

* fix comment

* throw instead of return

* add parnet to virtual node for contains lookup

* checck for actualNode before using

* can run through completely using real virtual node

* finalize

* use ternary

* add test, return null node
  • Loading branch information
straker committed Jun 10, 2019
1 parent af81897 commit 4e12217
Show file tree
Hide file tree
Showing 11 changed files with 371 additions and 59 deletions.
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)
: null;
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) {
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]);
}
result = pushNode(
result,
axe.utils.querySelectorAllFilter(candidate, selector, curried)
Expand Down
53 changes: 53 additions & 0 deletions test/core/base/rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,59 @@ describe('Rule', function() {
isNotCalled
);
});

it('should not be called when there is no actualNode', function() {
var rule = new Rule(
{
all: ['cats']
},
{
checks: {
cats: new Check({
id: 'cats',
evaluate: function() {}
})
}
}
);
rule.excludeHidden = false; // so we don't call utils.isHidden
var vNode = {
shadowId: undefined,
children: [],
parent: undefined,
_cache: {},
_isHidden: null,
_attrs: {
type: 'text',
autocomplete: 'not-on-my-watch'
},
props: {
nodeType: 1,
nodeName: 'input',
id: null,
type: 'text'
},
hasClass: function() {
return false;
},
attr: function(attrName) {
return this._attrs[attrName];
},
hasAttr: function(attrName) {
return !!this._attrs[attrName];
}
};
rule.runSync(
{
include: [vNode]
},
{},
function() {
assert.isFalse(isDqElementCalled);
},
isNotCalled
);
});
});

it('should pass thrown errors to the reject param', function() {
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

0 comments on commit 4e12217

Please sign in to comment.