UNPKG

axe-core

Version:

Accessibility engine for automated Web UI testing

195 lines (169 loc) 5.62 kB
import AbstractVirtualNode from './abstract-virtual-node'; import { isXHTML, validInputTypes } from '../../utils'; import { isFocusable, getTabbableElements } from '../../../commons/dom'; import cache from '../cache'; let isXHTMLGlobal; let nodeIndex = 0; class VirtualNode extends AbstractVirtualNode { /** * Wrap the real node and provide list of the flattened children * @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, parent, shadowId) { super(); this.shadowId = shadowId; this.children = []; this.actualNode = node; this.parent = parent; if (!parent) { nodeIndex = 0; } this.nodeIndex = nodeIndex++; this._isHidden = null; // will be populated by axe.utils.isHidden this._cache = {}; if (typeof isXHTMLGlobal === 'undefined') { isXHTMLGlobal = isXHTML(node.ownerDocument); } this._isXHTML = isXHTMLGlobal; // we will normalize the type prop for inputs by looking strictly // at the attribute and not what the browser resolves the type // to be if (node.nodeName.toLowerCase() === 'input') { let type = node.getAttribute('type'); type = this._isXHTML ? type : (type || '').toLowerCase(); if (!validInputTypes().includes(type)) { type = 'text'; } this._type = type; } if (cache.get('nodeMap')) { cache.get('nodeMap').set(node, this); } } // abstract Node properties so we can run axe in DOM-less environments. // add to the prototype so memory is shared across all virtual nodes get props() { const { nodeType, nodeName, id, multiple, nodeValue, value } = this.actualNode; return { nodeType, nodeName: this._isXHTML ? nodeName : nodeName.toLowerCase(), id, type: this._type, multiple, nodeValue, value }; } /** * Get the value of the given attribute name. * @param {String} attrName The name of the attribute. * @return {String|null} The value of the attribute or null if the attribute does not exist */ attr(attrName) { if (typeof this.actualNode.getAttribute !== 'function') { return null; } return this.actualNode.getAttribute(attrName); } /** * Determine if the element has the given attribute. * @param {String} attrName The name of the attribute * @return {Boolean} True if the element has the attribute, false otherwise. */ hasAttr(attrName) { if (typeof this.actualNode.hasAttribute !== 'function') { return false; } return this.actualNode.hasAttribute(attrName); } /** * Return a list of attribute names for the element. * @return {String[]} */ get attrNames() { if (!this._cache.hasOwnProperty('attrNames')) { let attrs; // eslint-disable-next-line no-restricted-syntax if (this.actualNode.attributes instanceof window.NamedNodeMap) { // eslint-disable-next-line no-restricted-syntax attrs = this.actualNode.attributes; } // if the attributes property is not of type NamedNodeMap // then the DOM has been clobbered. E.g. <form><input name="attributes"></form>. // We can clone the node to isolate it and then return // the attributes else { attrs = this.actualNode.cloneNode(false).attributes; } this._cache.attrNames = Array.from(attrs).map(attr => attr.name); } return this._cache.attrNames; } /** * Return a property of the computed style for this element and cache the result. This is much faster than called `getPropteryValue` every time. * @see https://jsperf.com/get-property-value * @return {String} */ getComputedStylePropertyValue(property) { const key = 'computedStyle_' + property; if (!this._cache.hasOwnProperty(key)) { if (!this._cache.hasOwnProperty('computedStyle')) { this._cache.computedStyle = window.getComputedStyle(this.actualNode); } this._cache[key] = this._cache.computedStyle.getPropertyValue(property); } return this._cache[key]; } /** * Determine if the element is focusable and cache the result. * @return {Boolean} True if the element is focusable, false otherwise. */ get isFocusable() { if (!this._cache.hasOwnProperty('isFocusable')) { this._cache.isFocusable = isFocusable(this.actualNode); } return this._cache.isFocusable; } /** * Return the list of tabbable elements for this element and cache the result. * @return {VirtualNode[]} */ get tabbableElements() { if (!this._cache.hasOwnProperty('tabbableElements')) { this._cache.tabbableElements = getTabbableElements(this); } return this._cache.tabbableElements; } /** * Return the client rects for this element and cache the result. * @return {DOMRect[]} */ get clientRects() { if (!this._cache.hasOwnProperty('clientRects')) { this._cache.clientRects = Array.from( this.actualNode.getClientRects() ).filter(rect => rect.width > 0); } return this._cache.clientRects; } /** * Return the bounding rect for this element and cache the result. * @return {DOMRect} */ get boundingClientRect() { if (!this._cache.hasOwnProperty('boundingClientRect')) { this._cache.boundingClientRect = this.actualNode.getBoundingClientRect(); } return this._cache.boundingClientRect; } } export default VirtualNode;