axe-core
Version:
Accessibility engine for automated Web UI testing
157 lines (145 loc) • 4.87 kB
JavaScript
/* global VirtualNode */
/*eslint no-use-before-define: 0*/
var axe = axe || { utils: {} };
/**
* This implemnts the flatten-tree algorithm specified:
* Originally here https://drafts.csswg.org/css-scoping/#flat-tree
* Hopefully soon published here: https://www.w3.org/TR/css-scoping-1/#flat-tree
*
* Some notable information:
******* NOTE: as of Chrome 59, this is broken in Chrome so that tests fail completely
******* removed functionality for now
* 1. <slot> elements do not have boxes by default (i.e. they do not get rendered and
* their CSS properties are ignored)
* 2. <slot> elements can be made to have a box by overriding the display property
* which is 'contents' by default
* 3. Even boxed <slot> elements do not show up in the accessibility tree until
* they have a tabindex applied to them OR they have a role applied to them AND
* they have a box (this is observed behavior in Safari on OS X, I cannot find
* the spec for this)
*/
/**
* find all the fallback content for a <slot> and return these as an array
* this array will also include any #text nodes
*
* @param node {Node} - the slot Node
* @return Array{Nodes}
*/
function getSlotChildren(node) {
var retVal = [];
node = node.firstChild;
while (node) {
retVal.push(node);
node = node.nextSibling;
}
return retVal;
}
/**
* Recursvely returns an array of the virtual DOM nodes at this level
* excluding comment nodes and the shadow DOM nodes <content> and <slot>
*
* @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, parent) {
// using a closure here and therefore cannot easily refactor toreduce the statements
var retVal, realArray, nodeName;
function reduceShadowDOM(res, child, parent) {
var replacements = flattenTree(child, shadowId, parent);
if (replacements) {
res = res.concat(replacements);
}
return res;
}
if (node.documentElement) {
// document
node = node.documentElement;
}
nodeName = node.nodeName.toLowerCase();
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, parent, shadowId);
shadowId =
'a' +
Math.random()
.toString()
.substring(2);
realArray = Array.from(node.shadowRoot.childNodes);
retVal.children = realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, retVal);
}, []);
return [retVal];
} else {
if (
nodeName === 'content' &&
typeof node.getDistributedNodes === 'function'
) {
realArray = Array.from(node.getDistributedNodes());
return realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, parent);
}, []);
} else if (
nodeName === 'slot' &&
typeof node.assignedNodes === 'function'
) {
realArray = Array.from(node.assignedNodes());
if (!realArray.length) {
// fallback content
realArray = getSlotChildren(node);
}
var styl = window.getComputedStyle(node);
// check the display property
if (false && styl.display !== 'contents') {
// intentionally commented out
// has a box
retVal = new VirtualNode(node, parent, shadowId);
retVal.children = realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, retVal);
}, []);
return [retVal];
} else {
return realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, parent);
}, []);
}
} else {
if (node.nodeType === 1) {
retVal = new VirtualNode(node, parent, shadowId);
realArray = Array.from(node.childNodes);
retVal.children = realArray.reduce((res, child) => {
return reduceShadowDOM(res, child, retVal);
}, []);
return [retVal];
} else if (node.nodeType === 3) {
// text
return [new VirtualNode(node, parent)];
}
return undefined;
}
}
}
/**
* Recursvely returns an array of the virtual DOM nodes at this level
* excluding comment nodes and the shadow DOM nodes <content> and <slot>
*
* @param {Node} node the current node
* @param {String} shadowId, optional ID of the shadow DOM that is the closest shadow
* ancestor of the node
*/
axe.utils.getFlattenedTree = function(node, shadowId) {
axe._cache.set('nodeMap', new WeakMap());
return flattenTree(node, shadowId);
};
/**
* Return a single node from the virtual dom tree
*
* @param {Object} vNode The flattened, virtual DOM tree
* @param {Node} node The HTML DOM node
*/
axe.utils.getNodeFromTree = function(vNode, node) {
const el = node || vNode;
return axe._cache.get('nodeMap') ? axe._cache.get('nodeMap').get(el) : null;
};