Skip to content

Commit 4e12217

Browse files
authored
feat(run-virtual-rule): new api to run rules using only virtual nodes (#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
1 parent af81897 commit 4e12217

File tree

11 files changed

+371
-59
lines changed

11 files changed

+371
-59
lines changed

lib/core/base/rule.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,9 @@ Rule.prototype.runSync = function(context, options = {}) {
282282

283283
const result = getResult(results);
284284
if (result) {
285-
result.node = new axe.utils.DqElement(node.actualNode, options);
285+
result.node = node.actualNode
286+
? new axe.utils.DqElement(node.actualNode, options)
287+
: null;
286288
ruleResult.nodes.push(result);
287289
}
288290
});

lib/core/base/virtual-node.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ const whitespaceRegex = /[\t\r\n\f]/g;
55
class VirtualNode {
66
/**
77
* Wrap the real node and provide list of the flattened children
8-
*
9-
* @param node {Node} the node in question
10-
* @param shadowId {String} the ID of the shadow DOM to which this node belongs
8+
* @param {Node} node the node in question
9+
* @param {VirtualNode} parent The parent VirtualNode
10+
* @param {String} shadowId the ID of the shadow DOM to which this node belongs
1111
*/
12-
constructor(node, shadowId) {
12+
constructor(node, parent, shadowId) {
1313
this.shadowId = shadowId;
1414
this.children = [];
1515
this.actualNode = node;
16+
this.parent = parent;
1617

1718
this._isHidden = null; // will be populated by axe.utils.isHidden
1819
this._cache = {};

lib/core/public/run-virtual-rule.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/* global helpers */
2+
3+
/**
4+
* Run a rule in a non-browser environment
5+
* @param {String} ruleId Id of the rule
6+
* @param {VirtualNode} vNode The virtual node to run the rule against
7+
* @param {Object} options (optional) Set of options passed into rules or checks
8+
* @return {Object} axe results for the rule run
9+
*/
10+
axe.runVirtualRule = function(ruleId, vNode, options = {}) {
11+
options.reporter = options.reporter || axe._audit.reporter || 'v1';
12+
axe._selectorData = {};
13+
14+
let rule = axe._audit.rules.find(rule => rule.id === ruleId);
15+
16+
if (!rule) {
17+
throw new Error('unknown rule `' + ruleId + '`');
18+
}
19+
20+
// rule.prototype.gather calls axe.utils.isHidden which in turn calls
21+
// window.getComputedStyle if the rule excludes hidden elements. we
22+
// can avoid this call by forcing the rule to not exclude hidden
23+
// elements
24+
rule = Object.create(rule, { excludeHidden: { value: false } });
25+
26+
const context = {
27+
include: [vNode]
28+
};
29+
30+
const rawResults = rule.runSync(context, options);
31+
axe.utils.publishMetaData(rawResults);
32+
axe.utils.finalizeRuleResult(rawResults);
33+
const results = axe.utils.aggregateResult([rawResults]);
34+
35+
results.violations.forEach(result =>
36+
result.nodes.forEach(nodeResult => {
37+
nodeResult.failureSummary = helpers.failureSummary(nodeResult);
38+
})
39+
);
40+
41+
return {
42+
...helpers.getEnvironmentData(),
43+
...results,
44+
toolOptions: options
45+
};
46+
};

lib/core/utils/contains.js

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,43 @@
22
* Wrapper for Node#contains; PhantomJS does not support Node#contains and erroneously reports that it does
33
* @method contains
44
* @memberof axe.utils
5-
* @param {HTMLElement} node The candidate container node
6-
* @param {HTMLElement} otherNode The node to test is contained by `node`
7-
* @return {Boolean} Whether `node` contains `otherNode`
5+
* @param {VirtualNode} vNode The candidate container VirtualNode
6+
* @param {VirtualNode} otherVNode The vNode to test is contained by `vNode`
7+
* @return {Boolean} Whether `vNode` contains `otherVNode`
88
*/
9-
axe.utils.contains = function(node, otherNode) {
9+
axe.utils.contains = function(vNode, otherVNode) {
1010
/*eslint no-bitwise: 0*/
1111
'use strict';
12-
function containsShadowChild(node, otherNode) {
13-
if (node.shadowId === otherNode.shadowId) {
12+
function containsShadowChild(vNode, otherVNode) {
13+
if (vNode.shadowId === otherVNode.shadowId) {
1414
return true;
1515
}
16-
return !!node.children.find(child => {
17-
return containsShadowChild(child, otherNode);
16+
return !!vNode.children.find(child => {
17+
return containsShadowChild(child, otherVNode);
1818
});
1919
}
2020

21-
if (node.shadowId || otherNode.shadowId) {
22-
return containsShadowChild(node, otherNode);
21+
if (vNode.shadowId || otherVNode.shadowId) {
22+
return containsShadowChild(vNode, otherVNode);
2323
}
2424

25-
if (typeof node.actualNode.contains === 'function') {
26-
return node.actualNode.contains(otherNode.actualNode);
25+
if (vNode.actualNode) {
26+
if (typeof vNode.actualNode.contains === 'function') {
27+
return vNode.actualNode.contains(otherVNode.actualNode);
28+
}
29+
30+
return !!(
31+
vNode.actualNode.compareDocumentPosition(otherVNode.actualNode) & 16
32+
);
33+
} else {
34+
// fallback for virtualNode only contexts (e.g. linting)
35+
// @see https://github.com/Financial-Times/polyfill-service/pull/183/files
36+
do {
37+
if (otherVNode === vNode) {
38+
return true;
39+
}
40+
} while ((otherVNode = otherVNode && otherVNode.parent));
2741
}
2842

29-
return !!(node.actualNode.compareDocumentPosition(otherNode.actualNode) & 16);
43+
return false;
3044
};

lib/core/utils/flattened-tree.js

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ function getSlotChildren(node) {
4545
* @param {Node} node the current node
4646
* @param {String} shadowId, optional ID of the shadow DOM that is the closest shadow
4747
* ancestor of the node
48+
* @param {VirtualNode} parent the parent VirtualNode
4849
*/
49-
function flattenTree(node, shadowId) {
50+
function flattenTree(node, shadowId, parent) {
5051
// using a closure here and therefore cannot easily refactor toreduce the statements
5152
var retVal, realArray, nodeName;
52-
function reduceShadowDOM(res, child) {
53-
var replacements = flattenTree(child, shadowId);
53+
function reduceShadowDOM(res, child, parent) {
54+
var replacements = flattenTree(child, shadowId, parent);
5455
if (replacements) {
5556
res = res.concat(replacements);
5657
}
@@ -66,22 +67,24 @@ function flattenTree(node, shadowId) {
6667
if (axe.utils.isShadowRoot(node)) {
6768
// generate an ID for this shadow root and overwrite the current
6869
// closure shadowId with this value so that it cascades down the tree
69-
retVal = new VirtualNode(node, shadowId);
70+
retVal = new VirtualNode(node, parent, shadowId);
7071
shadowId =
7172
'a' +
7273
Math.random()
7374
.toString()
7475
.substring(2);
7576
realArray = Array.from(node.shadowRoot.childNodes);
76-
retVal.children = realArray.reduce(reduceShadowDOM, []);
77+
retVal.children = realArray.reduce((res, child) => {
78+
return reduceShadowDOM(res, child, retVal);
79+
}, []);
80+
7781
return [retVal];
7882
} else {
79-
if (
80-
nodeName === 'content' &&
81-
typeof node.getDistributedNodes === 'function'
82-
) {
83+
if (nodeName === 'content') {
8384
realArray = Array.from(node.getDistributedNodes());
84-
return realArray.reduce(reduceShadowDOM, []);
85+
return realArray.reduce((res, child) => {
86+
return reduceShadowDOM(res, child, parent);
87+
}, []);
8588
} else if (
8689
nodeName === 'slot' &&
8790
typeof node.assignedNodes === 'function'
@@ -96,21 +99,29 @@ function flattenTree(node, shadowId) {
9699
if (false && styl.display !== 'contents') {
97100
// intentionally commented out
98101
// has a box
99-
retVal = new VirtualNode(node, shadowId);
100-
retVal.children = realArray.reduce(reduceShadowDOM, []);
102+
retVal = new VirtualNode(node, parent, shadowId);
103+
retVal.children = realArray.reduce((res, child) => {
104+
return reduceShadowDOM(res, child, retVal);
105+
}, []);
106+
101107
return [retVal];
102108
} else {
103-
return realArray.reduce(reduceShadowDOM, []);
109+
return realArray.reduce((res, child) => {
110+
return reduceShadowDOM(res, child, parent);
111+
}, []);
104112
}
105113
} else {
106114
if (node.nodeType === 1) {
107-
retVal = new VirtualNode(node, shadowId);
115+
retVal = new VirtualNode(node, parent, shadowId);
108116
realArray = Array.from(node.childNodes);
109-
retVal.children = realArray.reduce(reduceShadowDOM, []);
117+
retVal.children = realArray.reduce((res, child) => {
118+
return reduceShadowDOM(res, child, retVal);
119+
}, []);
120+
110121
return [retVal];
111122
} else if (node.nodeType === 3) {
112123
// text
113-
return [new VirtualNode(node)];
124+
return [new VirtualNode(node, parent)];
114125
}
115126
return undefined;
116127
}

lib/core/utils/select.js

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ function getDeepest(collection) {
2525
function isNodeInContext(node, context) {
2626
'use strict';
2727

28-
var include =
28+
const include =
2929
context.include &&
3030
getDeepest(
3131
context.include.filter(function(candidate) {
3232
return axe.utils.contains(candidate, node);
3333
})
3434
);
35-
var exclude =
35+
const exclude =
3636
context.exclude &&
3737
getDeepest(
3838
context.exclude.filter(function(candidate) {
@@ -58,7 +58,7 @@ function isNodeInContext(node, context) {
5858
function pushNode(result, nodes) {
5959
'use strict';
6060

61-
var temp;
61+
let temp;
6262

6363
if (result.length === 0) {
6464
return nodes;
@@ -69,7 +69,7 @@ function pushNode(result, nodes) {
6969
result = nodes;
7070
nodes = temp;
7171
}
72-
for (var i = 0, l = nodes.length; i < l; i++) {
72+
for (let i = 0, l = nodes.length; i < l; i++) {
7373
if (!result.includes(nodes[i])) {
7474
result.push(nodes[i]);
7575
}
@@ -84,10 +84,7 @@ function pushNode(result, nodes) {
8484
*/
8585
function reduceIncludes(includes) {
8686
return includes.reduce((res, el) => {
87-
if (
88-
!res.length ||
89-
!res[res.length - 1].actualNode.contains(el.actualNode)
90-
) {
87+
if (!res.length || !axe.utils.contains(res[res.length - 1], el)) {
9188
res.push(el);
9289
}
9390
return res;
@@ -103,33 +100,26 @@ function reduceIncludes(includes) {
103100
axe.utils.select = function select(selector, context) {
104101
'use strict';
105102

106-
var result = [],
107-
candidate;
103+
let result = [];
104+
let candidate;
108105
if (axe._selectCache) {
109106
// if used outside of run, it will still work
110-
for (var j = 0, l = axe._selectCache.length; j < l; j++) {
107+
for (let j = 0, l = axe._selectCache.length; j < l; j++) {
111108
// First see whether the item exists in the cache
112-
let item = axe._selectCache[j];
109+
const item = axe._selectCache[j];
113110
if (item.selector === selector) {
114111
return item.result;
115112
}
116113
}
117114
}
118-
var curried = (function(context) {
115+
const curried = (function(context) {
119116
return function(node) {
120117
return isNodeInContext(node, context);
121118
};
122119
})(context);
123-
var reducedIncludes = reduceIncludes(context.include);
124-
for (var i = 0; i < reducedIncludes.length; i++) {
120+
const reducedIncludes = reduceIncludes(context.include);
121+
for (let i = 0; i < reducedIncludes.length; i++) {
125122
candidate = reducedIncludes[i];
126-
if (
127-
candidate.actualNode.nodeType === candidate.actualNode.ELEMENT_NODE &&
128-
axe.utils.matchesSelector(candidate.actualNode, selector) &&
129-
curried(candidate)
130-
) {
131-
result = pushNode(result, [candidate]);
132-
}
133123
result = pushNode(
134124
result,
135125
axe.utils.querySelectorAllFilter(candidate, selector, curried)

test/core/base/rule.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,59 @@ describe('Rule', function() {
12191219
isNotCalled
12201220
);
12211221
});
1222+
1223+
it('should not be called when there is no actualNode', function() {
1224+
var rule = new Rule(
1225+
{
1226+
all: ['cats']
1227+
},
1228+
{
1229+
checks: {
1230+
cats: new Check({
1231+
id: 'cats',
1232+
evaluate: function() {}
1233+
})
1234+
}
1235+
}
1236+
);
1237+
rule.excludeHidden = false; // so we don't call utils.isHidden
1238+
var vNode = {
1239+
shadowId: undefined,
1240+
children: [],
1241+
parent: undefined,
1242+
_cache: {},
1243+
_isHidden: null,
1244+
_attrs: {
1245+
type: 'text',
1246+
autocomplete: 'not-on-my-watch'
1247+
},
1248+
props: {
1249+
nodeType: 1,
1250+
nodeName: 'input',
1251+
id: null,
1252+
type: 'text'
1253+
},
1254+
hasClass: function() {
1255+
return false;
1256+
},
1257+
attr: function(attrName) {
1258+
return this._attrs[attrName];
1259+
},
1260+
hasAttr: function(attrName) {
1261+
return !!this._attrs[attrName];
1262+
}
1263+
};
1264+
rule.runSync(
1265+
{
1266+
include: [vNode]
1267+
},
1268+
{},
1269+
function() {
1270+
assert.isFalse(isDqElementCalled);
1271+
},
1272+
isNotCalled
1273+
);
1274+
});
12221275
});
12231276

12241277
it('should pass thrown errors to the reject param', function() {

test/core/base/virtual-node.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@ describe('VirtualNode', function() {
1111
assert.isFunction(VirtualNode);
1212
});
1313

14-
it('should accept two parameters', function() {
15-
assert.lengthOf(VirtualNode, 2);
14+
it('should accept three parameters', function() {
15+
assert.lengthOf(VirtualNode, 3);
1616
});
1717

1818
describe('prototype', function() {
1919
it('should have public properties', function() {
20-
var vNode = new VirtualNode(node, 'foo');
20+
var parent = {};
21+
var vNode = new VirtualNode(node, parent, 'foo');
2122

2223
assert.equal(vNode.shadowId, 'foo');
2324
assert.typeOf(vNode.children, 'array');
2425
assert.equal(vNode.actualNode, node);
26+
assert.equal(vNode.parent, parent);
2527
});
2628

2729
it('should abstract Node properties', function() {

0 commit comments

Comments
 (0)