Skip to content

Commit

Permalink
fix(performance): significantly improve the performance of the dom.fi…
Browse files Browse the repository at this point in the history
…ndUp utility fixes #696
  • Loading branch information
dylanb committed Jan 28, 2018
1 parent b553e70 commit 9197e03
Show file tree
Hide file tree
Showing 21 changed files with 342 additions and 121 deletions.
12 changes: 6 additions & 6 deletions lib/checks/aria/required-parent.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ function getSelector(role) {
return impliedNative.concat('[role="' + role + '"]').join(',');
}

function getMissingContext(element, requiredContext, includeElement) {
function getMissingContext(virtualNode, requiredContext, includeElement) {
var index, length,
role = element.getAttribute('role'),
role = virtualNode.actualNode.getAttribute('role'),
missing = [];

if (!requiredContext) {
Expand All @@ -15,10 +15,10 @@ function getMissingContext(element, requiredContext, includeElement) {
if (!requiredContext) { return null; }

for (index = 0, length = requiredContext.length; index < length; index++) {
if (includeElement && axe.utils.matchesSelector(element, getSelector(requiredContext[index]))) {
if (includeElement && axe.utils.matchesSelector(virtualNode.actualNode, getSelector(requiredContext[index]))) {
return null;
}
if (axe.commons.dom.findUp(element, getSelector(requiredContext[index]))) {
if (axe.commons.dom.findUpVirtual(virtualNode, getSelector(requiredContext[index]))) {
//if one matches, it passes
return null;
} else {
Expand Down Expand Up @@ -46,15 +46,15 @@ function getAriaOwners(element) {
return owners.length ? owners : null;
}

var missingParents = getMissingContext(node);
var missingParents = getMissingContext(virtualNode);

if (!missingParents) { return true; }

var owners = getAriaOwners(node);

if (owners) {
for (var i = 0, l = owners.length; i < l; i++) {
missingParents = getMissingContext(owners[i], missingParents, true);
missingParents = getMissingContext(axe.utils.getNodeFromTree(axe._tree[0], owners[i]), missingParents, true);
if (!missingParents) { return true; }
}
}
Expand Down
18 changes: 9 additions & 9 deletions lib/checks/forms/fieldset.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,22 @@ function spliceCurrentNode(nodes, current) {
});
}

function runCheck(element) {
const name = axe.commons.utils.escapeSelector(node.name);
const root = axe.commons.dom.getRootNode(node);
function runCheck(virtualNode) {
const name = axe.commons.utils.escapeSelector(virtualNode.actualNode.name);
const root = axe.commons.dom.getRootNode(virtualNode.actualNode);
const matchingNodes = root.querySelectorAll('input[type="' +
axe.commons.utils.escapeSelector(node.type) + '"][name="' + name + '"]');
axe.commons.utils.escapeSelector(virtualNode.actualNode.type) + '"][name="' + name + '"]');

if (matchingNodes.length < 2) {
return true;
}
const fieldset = axe.commons.dom.findUp(element, 'fieldset');
const group = axe.commons.dom.findUp(element, '[role="group"]' +
(node.type === 'radio' ? ',[role="radiogroup"]' : ''));
const fieldset = axe.commons.dom.findUpVirtual(virtualNode, 'fieldset');
const group = axe.commons.dom.findUpVirtual(virtualNode, '[role="group"]' +
(virtualNode.actualNode.type === 'radio' ? ',[role="radiogroup"]' : ''));

if (!group && !fieldset) {
failureCode = 'no-group';
self.relatedNodes(spliceCurrentNode(matchingNodes, element));
self.relatedNodes(spliceCurrentNode(matchingNodes, virtualNode.actualNode));
return false;

} else if (fieldset) {
Expand All @@ -86,7 +86,7 @@ var data = {
type: node.getAttribute('type')
};

var result = runCheck(node);
var result = runCheck(virtualNode);
if (!result) {
data.failureCode = failureCode;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/label/implicit.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

var label = axe.commons.dom.findUp(node, 'label');
var label = axe.commons.dom.findUpVirtual(virtualNode, 'label');
if (label) {
return !!axe.commons.text.accessibleText(label);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/navigation/p-as-heading.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ if (!nextStyle || !isHeaderStyle(currStyle, nextStyle, margins)) {
return true;
}

let blockquote = axe.commons.dom.findUp(node, 'blockquote');
let blockquote = axe.commons.dom.findUpVirtual(virtualNode, 'blockquote');
if (blockquote && blockquote.nodeName.toUpperCase() === 'BLOCKQUOTE') {
return undefined;
}
Expand Down
48 changes: 34 additions & 14 deletions lib/commons/dom/find-up.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,54 @@
/* global dom, axe */
/**
* recusively walk up the DOM, checking for a node which matches a selector
* Find the virtual node and call dom.fundUpVirtual
*
* **WARNING:** this should be used sparingly, as it's not even close to being performant
* @method findUp
* @memberof axe.commons.dom
* @instance
* @param {HTMLElement|String} element The starting HTMLElement
* @param {HTMLElement} element The starting HTMLElement
* @param {String} target The selector for the HTMLElement
* @return {HTMLElement|null} Either the matching HTMLElement or `null` if there was no match
*/
dom.findUp = function (element, target) {
let doc, matches,
parent = element;
return dom.findUpVirtual(axe.utils.getNodeFromTree(axe._tree[0], element), target);
};

/**
* recusively walk up the DOM, checking for a node which matches a selector
*
* **WARNING:** this should be used sparingly, as it's not even close to being performant
* @method findUpVirtual
* @memberof axe.commons.dom
* @instance
* @param {VirtualNode} element The starting virtualNode
* @param {String} target The selector for the HTMLElement
* @return {HTMLElement|null} Either the matching HTMLElement or `null` if there was no match
*/
dom.findUpVirtual = function (element, target) {
let parent;

parent = element.actualNode;
// virtualNode will have a shadowId if the element lives inside a shadow DOM or is
// slotted into a shadow DOM
if (!element.shadowId && typeof element.actualNode.closest === 'function') {
// non-shadow DOM elements
let match = element.actualNode.closest(target);
if (match) {
return match;
}
return null;
}
// handle shadow DOM elements and older browsers
do {// recursively walk up the DOM, checking each parent node
parent = (parent.assignedSlot ? parent.assignedSlot : parent.parentNode);
if (parent && parent.nodeType === 11) {
matches = null;
parent = parent.host;
}
if (!matches) {
doc = axe.commons.dom.getRootNode(parent);
matches = doc.querySelectorAll(target);
matches = axe.utils.toArray(matches);
if (doc === document && !matches.length) {
return null;
}
}
} while (parent && !matches.includes(parent));
} while (parent && !axe.utils.matchesSelector(parent, target) && parent !== document.documentElement);

if (!axe.utils.matchesSelector(parent, target)) {
return null;
}
return parent;
};
8 changes: 4 additions & 4 deletions lib/commons/text/accessible-text-virtual.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ var phrasingElements = ['A', 'EM', 'STRONG', 'SMALL', 'MARK', 'ABBR', 'DFN', 'I'
* @param {HTMLElement} element The HTMLElement
* @return {HTMLElement} The label element, or null if none is found
*/
function findLabel({ actualNode }) {
function findLabel(virtualNode) {
let label;
if (actualNode.id) {
if (virtualNode.actualNode.id) {
label = dom.findElmsInContext({
elm: 'label', attr: 'for', value: actualNode.id, context: actualNode
elm: 'label', attr: 'for', value: virtualNode.actualNode.id, context: virtualNode.actualNode
})[0];
} else {
label = dom.findUp(actualNode, 'label');
label = dom.findUpVirtual(virtualNode, 'label');
}
return axe.utils.getNodeFromTree(axe._tree[0], label);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/commons/text/label-virtual.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ text.labelVirtual = function (node) {
}
}

ref = dom.findUp(node.actualNode, 'label');
ref = dom.findUpVirtual(node, 'label');
candidate = ref && text.visible(ref, true);
if (candidate) {
return candidate;
Expand Down
27 changes: 21 additions & 6 deletions lib/core/utils/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,29 @@ function isNodeInContext(node, context) {
function pushNode(result, nodes, context) {
'use strict';

var temp;
var curried = (function (context) {
return function (node) {
return isNodeInContext(node, context);
};
})(context);
nodes = nodes.filter(curried);

if (result.length === 0) {
return nodes;
}
if (result.length < nodes.length) {
// switch so the comparison is shortest
temp = result;
result = nodes;
nodes = temp;
}
for (var i = 0, l = nodes.length; i < l; i++) {
//jshint loopfunc:true
if (!result.find(function (item) {
return item.actualNode === nodes[i].actualNode;
}) && isNodeInContext(nodes[i], context)) {
if (!result.includes(nodes[i])) {
result.push(nodes[i]);
}
}
return result;
}

/**
Expand All @@ -72,9 +87,9 @@ axe.utils.select = function select(selector, context) {
candidate = context.include[i];
if (candidate.actualNode.nodeType === candidate.actualNode.ELEMENT_NODE &&
axe.utils.matchesSelector(candidate.actualNode, selector)) {
pushNode(result, [candidate], context);
result = pushNode(result, [candidate], context);
}
pushNode(result, axe.utils.querySelectorAll(candidate, selector), context);
result = pushNode(result, axe.utils.querySelectorAll(candidate, selector), context);
}

return result.sort(axe.utils.nodeSorter);
Expand Down
11 changes: 7 additions & 4 deletions lib/rules/color-contrast-matches.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
var nodeName = node.nodeName.toUpperCase(),
nodeType = node.type;

if (node.getAttribute('aria-disabled') === 'true' || axe.commons.dom.findUp(node, '[aria-disabled="true"]')) {
if (node.getAttribute('aria-disabled') === 'true' ||
axe.commons.dom.findUpVirtual(virtualNode, '[aria-disabled="true"]')) {
return false;
}

Expand All @@ -23,16 +24,18 @@ if (nodeName === 'OPTION') {
return false;
}

if (nodeName === 'BUTTON' && node.disabled || axe.commons.dom.findUp(node, 'button[disabled]')) {
if (nodeName === 'BUTTON' && node.disabled ||
axe.commons.dom.findUpVirtual(virtualNode, 'button[disabled]')) {
return false;
}

if (nodeName === 'FIELDSET' && node.disabled || axe.commons.dom.findUp(node, 'fieldset[disabled]')) {
if (nodeName === 'FIELDSET' && node.disabled ||
axe.commons.dom.findUpVirtual(virtualNode, 'fieldset[disabled]')) {
return false;
}

// check if the element is a label or label descendant for a disabled control
var nodeParentLabel = axe.commons.dom.findUp(node, 'label');
var nodeParentLabel = axe.commons.dom.findUpVirtual(virtualNode, 'label');
if (nodeName === 'LABEL' || nodeParentLabel) {
var relevantNode = node;
var relevantVirtualNode = virtualNode;
Expand Down
1 change: 1 addition & 0 deletions test/checks/aria/required-parent.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('aria-required-parent', function () {
afterEach(function () {
fixture.innerHTML = '';
checkContext._data = null;
axe._tree = undefined;
});

it('should detect missing required parent', function () {
Expand Down
11 changes: 8 additions & 3 deletions test/checks/color/color-contrast.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ describe('color-contrast', function () {
'use strict';

var fixture = document.getElementById('fixture');
var fixtureSetup = axe.testUtils.fixtureSetup;

var checkContext = {
_relatedNodes: [],
Expand All @@ -18,6 +19,7 @@ describe('color-contrast', function () {
fixture.innerHTML = '';
checkContext._relatedNodes = [];
checkContext._data = null;
axe._tree = undefined;
});

it('should return the proper values stored in data', function () {
Expand Down Expand Up @@ -206,13 +208,14 @@ describe('color-contrast', function () {
});

it('should return true when a label wraps a text input', function () {
fixture.innerHTML = '<label id="target">' +
'My text <input type="text"></label>';
fixtureSetup('<label id="target">' +
'My text <input type="text"></label>');
var target = fixture.querySelector('#target');
var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], target);
if (window.PHANTOMJS) {
assert.ok('PhantomJS is a liar');
} else {
var result = checks['color-contrast'].evaluate.call(checkContext, target);
var result = checks['color-contrast'].evaluate.call(checkContext, target, {}, virtualNode);
assert.isTrue(result);
}
});
Expand All @@ -228,13 +231,15 @@ describe('color-contrast', function () {
it('should return true when there is sufficient contrast based on thead', function () {
fixture.innerHTML = '<table><thead style="background: #d00d2c"><tr><th id="target" style="color: #fff; padding: .5em">Col 1</th></tr></thead></table>';
var target = fixture.querySelector('#target');
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target));
assert.deepEqual(checkContext._relatedNodes, []);
});

it('should return true when there is sufficient contrast based on tbody', function () {
fixture.innerHTML = '<table><tbody style="background: #d00d2c"><tr><td id="target" style="color: #fff; padding: .5em">Col 1</td></tr></tbody></table>';
var target = fixture.querySelector('#target');
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target));
assert.deepEqual(checkContext._relatedNodes, []);
});
Expand Down
Loading

0 comments on commit 9197e03

Please sign in to comment.