diff --git a/lib/checks/forms/autocomplete-appropriate.js b/lib/checks/forms/autocomplete-appropriate.js index ba44857169..0a5590babb 100644 --- a/lib/checks/forms/autocomplete-appropriate.js +++ b/lib/checks/forms/autocomplete-appropriate.js @@ -1,5 +1,5 @@ // Select and textarea is always allowed -if (virtualNode.elementNodeName !== 'input') { +if (virtualNode.props.nodeName !== 'input') { return true; } diff --git a/lib/core/base/virtual-node.js b/lib/core/base/virtual-node.js index 8aa998d783..f5df8f6736 100644 --- a/lib/core/base/virtual-node.js +++ b/lib/core/base/virtual-node.js @@ -17,18 +17,24 @@ class VirtualNode { this._isHidden = null; // will be populated by axe.utils.isHidden this._cache = {}; - // abstract Node and Element APIs so we can run axe in DOM-less - // environments. these are static properties in the assumption - // that axe does not change any of them while it runs. - this.elementNodeType = node.nodeType; - this.elementNodeName = node.nodeName.toLowerCase(); - this.elementId = node.id; - if (axe._cache.get('nodeMap')) { axe._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, type } = this.actualNode; + + return { + nodeType, + nodeName: nodeName.toLowerCase(), + id, + type + }; + } + /** * Determine if the actualNode has the given class name. * @see https://j11y.io/jquery/#v=2.0.3&fn=jQuery.fn.hasClass @@ -66,7 +72,7 @@ class VirtualNode { /** * Determine if the element has the given attribute. * @param {String} attrName The name of the attribute - * @return {Bool} True if the element has the attribute, false otherwise. + * @return {Boolean} True if the element has the attribute, false otherwise. */ hasAttr(attrName) { if (typeof this.actualNode.hasAttribute !== 'function') { diff --git a/lib/core/utils/qsa.js b/lib/core/utils/qsa.js index b4d4df89ed..a9d4152bf3 100644 --- a/lib/core/utils/qsa.js +++ b/lib/core/utils/qsa.js @@ -6,8 +6,8 @@ var matchExpressions = function() {}; function matchesTag(vNode, exp) { return ( - vNode.elementNodeType === 1 && - (exp.tag === '*' || vNode.elementNodeName === exp.tag) + vNode.props.nodeType === 1 && + (exp.tag === '*' || vNode.props.nodeName === exp.tag) ); } @@ -26,7 +26,7 @@ function matchesAttributes(vNode, exp) { } function matchesId(vNode, exp) { - return !exp.id || vNode.elementId === exp.id; + return !exp.id || vNode.props.id === exp.id; } function matchesPseudos(target, exp) { diff --git a/lib/rules/autocomplete-matches.js b/lib/rules/autocomplete-matches.js index ace7eec2d2..8353418372 100644 --- a/lib/rules/autocomplete-matches.js +++ b/lib/rules/autocomplete-matches.js @@ -1,31 +1,34 @@ const { text, aria, dom } = axe.commons; -const autocomplete = node.getAttribute('autocomplete'); +const autocomplete = virtualNode.attr('autocomplete'); if (!autocomplete || text.sanitize(autocomplete) === '') { return false; } -const nodeName = node.nodeName.toUpperCase(); -if (['TEXTAREA', 'INPUT', 'SELECT'].includes(nodeName) === false) { +const nodeName = virtualNode.props.nodeName; +if (['textarea', 'input', 'select'].includes(nodeName) === false) { return false; } // The element is an `input` element a `type` of `hidden`, `button`, `submit` or `reset` const excludedInputTypes = ['submit', 'reset', 'button', 'hidden']; -if (nodeName === 'INPUT' && excludedInputTypes.includes(node.type)) { +if ( + nodeName === 'input' && + excludedInputTypes.includes(virtualNode.props.type) +) { return false; } // The element has a `disabled` or `aria-disabled="true"` attribute -const ariaDisabled = node.getAttribute('aria-disabled') || 'false'; -if (node.disabled || ariaDisabled.toLowerCase() === 'true') { +const ariaDisabled = virtualNode.attr('aria-disabled') || 'false'; +if (virtualNode.hasAttr('disabled') || ariaDisabled.toLowerCase() === 'true') { return false; } // The element has `tabindex="-1"` and has a [[semantic role]] that is // not a [widget](https://www.w3.org/TR/wai-aria-1.1/#widget_roles) -const role = node.getAttribute('role'); -const tabIndex = node.getAttribute('tabindex'); +const role = virtualNode.attr('role'); +const tabIndex = virtualNode.attr('tabindex'); if (tabIndex === '-1' && role) { const roleDef = aria.lookupTable.role[role]; if (roleDef === undefined || roleDef.type !== 'widget') { @@ -36,8 +39,9 @@ if (tabIndex === '-1' && role) { // The element is **not** visible on the page or exposed to assistive technologies if ( tabIndex === '-1' && - !dom.isVisible(node, false) && - !dom.isVisible(node, true) + virtualNode.actualNode && + !dom.isVisible(virtualNode.actualNode, false) && + !dom.isVisible(virtualNode.actualNode, true) ) { return false; } diff --git a/test/core/base/virtual-node.js b/test/core/base/virtual-node.js index da2df876bf..71b7cf4b34 100644 --- a/test/core/base/virtual-node.js +++ b/test/core/base/virtual-node.js @@ -24,13 +24,16 @@ describe('VirtualNode', function() { assert.equal(vNode.actualNode, node); }); - it('should abstract Node and Element APIs', function() { + it('should abstract Node properties', function() { + node = document.createElement('input'); node.id = 'monkeys'; var vNode = new VirtualNode(node); - assert.equal(vNode.elementNodeType, 1); - assert.equal(vNode.elementNodeName, 'div'); - assert.equal(vNode.elementId, 'monkeys'); + assert.isDefined(vNode.props); + assert.equal(vNode.props.nodeType, 1); + assert.equal(vNode.props.nodeName, 'input'); + assert.equal(vNode.props.id, 'monkeys'); + assert.equal(vNode.props.type, 'text'); }); it('should lowercase nodeName', function() { @@ -39,7 +42,7 @@ describe('VirtualNode', function() { }; var vNode = new VirtualNode(node); - assert.equal(vNode.elementNodeName, 'foobar'); + assert.equal(vNode.props.nodeName, 'foobar'); }); describe('hasClass', function() { diff --git a/test/rule-matches/autocomplete-matches.js b/test/rule-matches/autocomplete-matches.js index e4343c601e..66cf1b7da9 100644 --- a/test/rule-matches/autocomplete-matches.js +++ b/test/rule-matches/autocomplete-matches.js @@ -1,6 +1,7 @@ describe('autocomplete-matches', function() { 'use strict'; var fixture = document.getElementById('fixture'); + var queryFixture = axe.testUtils.queryFixture; var rule = axe._audit.rules.find(function(rule) { return rule.id === 'autocomplete-valid'; }); @@ -14,105 +15,88 @@ describe('autocomplete-matches', function() { }); it('returns true for input elements', function() { - var elm = document.createElement('input'); - elm.setAttribute('autocomplete', 'foo'); - fixture.appendChild(elm); - assert.isTrue(rule.matches(elm)); + var vNode = queryFixture(''); + assert.isTrue(rule.matches(null, vNode)); }); it('returns true for select elements', function() { - var elm = document.createElement('select'); - elm.setAttribute('autocomplete', 'foo'); - fixture.appendChild(elm); - assert.isTrue(rule.matches(elm)); + var vNode = queryFixture('