From b3e08730f60e35cf70a4b0e0324b0bd6da5edd19 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 30 May 2019 09:30:37 -0600 Subject: [PATCH] feat(autocomplete-valid): allow autocomplete-valid to be run entirely off of a virtual node (#1591) * feat(rule): new api to run rules using only virtual nodes * only do autocomplete * add tests for hasAttr --- lib/checks/forms/autocomplete-appropriate.js | 8 ++--- lib/checks/forms/autocomplete-valid.js | 2 +- lib/core/base/virtual-node.js | 25 +++++++++++---- test/core/base/virtual-node.js | 32 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/lib/checks/forms/autocomplete-appropriate.js b/lib/checks/forms/autocomplete-appropriate.js index b01eca4910..ba44857169 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 (node.nodeName.toUpperCase() !== 'INPUT') { +if (virtualNode.elementNodeName !== 'input') { return true; } @@ -34,7 +34,7 @@ if (typeof options === 'object') { }); } -const autocomplete = node.getAttribute('autocomplete'); +const autocomplete = virtualNode.attr('autocomplete'); const autocompleteTerms = autocomplete .split(/\s+/g) .map(term => term.toLowerCase()); @@ -55,8 +55,8 @@ const allowedTypes = allowedTypesMap[purposeTerm]; * Reference HTML Spec - https://html.spec.whatwg.org/multipage/input.html#the-input-element to filter allowed values for `type` * and sanitize (https://html.spec.whatwg.org/multipage/input.html#value-sanitization-algorithm) */ -let type = node.hasAttribute('type') - ? axe.commons.text.sanitize(node.getAttribute('type')).toLowerCase() +let type = virtualNode.hasAttr('type') + ? axe.commons.text.sanitize(virtualNode.attr('type')).toLowerCase() : 'text'; type = axe.utils.validInputTypes().includes(type) ? type : 'text'; diff --git a/lib/checks/forms/autocomplete-valid.js b/lib/checks/forms/autocomplete-valid.js index 4b7617d7e7..300a5fb339 100644 --- a/lib/checks/forms/autocomplete-valid.js +++ b/lib/checks/forms/autocomplete-valid.js @@ -1,2 +1,2 @@ -const autocomplete = node.getAttribute('autocomplete') || ''; +const autocomplete = virtualNode.attr('autocomplete') || ''; return axe.commons.text.isValidAutocomplete(autocomplete, options); diff --git a/lib/core/base/virtual-node.js b/lib/core/base/virtual-node.js index c2cc1f1b48..279b4700fb 100644 --- a/lib/core/base/virtual-node.js +++ b/lib/core/base/virtual-node.js @@ -6,8 +6,8 @@ class VirtualNode { /** * Wrap the real node and provide list of the flattened children * - * @param node {Node} - the node in question - * @param shadowId {String} - the ID of the shadow DOM to which this node belongs + * @param node {Node} the node in question + * @param shadowId {String} the ID of the shadow DOM to which this node belongs */ constructor(node, shadowId) { this.shadowId = shadowId; @@ -32,7 +32,7 @@ class VirtualNode { /** * Determine if the actualNode has the given class name. * @see https://j11y.io/jquery/#v=2.0.3&fn=jQuery.fn.hasClass - * @param {String} className - The class to check for. + * @param {String} className The class to check for. * @return {Boolean} True if the actualNode has the given class, false otherwise. */ hasClass(className) { @@ -50,8 +50,8 @@ class VirtualNode { /** * Get the value of the given attribute name. - * @param {String} attrName - The name of the attribute. - * @returns {String|null} The value of the attribute or null if the attribute does not exist + * @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') { @@ -61,6 +61,19 @@ class VirtualNode { return this.actualNode.getAttribute(attrName); } + /** + * 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. + */ + hasAttr(attrName) { + if (typeof this.actualNode.hasAttribute !== 'function') { + return false; + } + + return this.actualNode.hasAttribute(attrName); + } + /** * Determine if the element is focusable and cache the result. * @return {Boolean} True if the element is focusable, false otherwise. @@ -74,7 +87,7 @@ class VirtualNode { /** * Return the list of tabbable elements for this element and cache the result. - * @returns {VirtualNode[]} + * @return {VirtualNode[]} */ get tabbableElements() { if (!this._cache.hasOwnProperty('tabbableElements')) { diff --git a/test/core/base/virtual-node.js b/test/core/base/virtual-node.js index 7ed2d21822..d3c22084d3 100644 --- a/test/core/base/virtual-node.js +++ b/test/core/base/virtual-node.js @@ -130,6 +130,38 @@ describe('VirtualNode', function() { }); }); + describe('hasAttr', function() { + it('should return true if the element has the attribute', function() { + node.setAttribute('foo', 'bar'); + var vNode = new VirtualNode(node); + + assert.isTrue(vNode.hasAttr('foo')); + }); + + it('should return false if the element does not have the attribute', function() { + var vNode = new VirtualNode(node); + + assert.isFalse(vNode.hasAttr('foo')); + }); + + it('should return false for text nodes', function() { + node.textContent = 'hello'; + var vNode = new VirtualNode(node.firstChild); + + assert.isFalse(vNode.hasAttr('foo')); + }); + + it('should return false if hasAttribute is not a function', function() { + var node = { + nodeName: 'DIV', + hasAttribute: null + }; + var vNode = new VirtualNode(node); + + assert.isFalse(vNode.hasAttr('foo')); + }); + }); + describe('isFocusable', function() { var commons;