From 85e57eb31018a5235e7517b203570eb3eaa3e870 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 14 Jan 2020 11:06:34 -0700 Subject: [PATCH 1/7] fix(matchers): use VirtualNode functions --- lib/commons/matches/attributes.js | 7 +++++-- lib/commons/matches/from-definition.js | 2 +- lib/commons/matches/node-name.js | 8 ++++++-- lib/commons/matches/properties.js | 9 ++++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/commons/matches/attributes.js b/lib/commons/matches/attributes.js index b8d2a043c7..93428089f1 100644 --- a/lib/commons/matches/attributes.js +++ b/lib/commons/matches/attributes.js @@ -18,6 +18,9 @@ * @returns {Boolean} */ matches.attributes = function matchesAttributes(node, matcher) { - node = node.actualNode || node; - return matches.fromFunction(attrName => node.getAttribute(attrName), matcher); + let vNode = + node instanceof axe.AbstractVirtualNode + ? node + : axe.utils.getNodeFromTree(node); + return matches.fromFunction(attrName => vNode.attr(attrName), matcher); }; diff --git a/lib/commons/matches/from-definition.js b/lib/commons/matches/from-definition.js index 09044dc00a..0d67c2b4d8 100644 --- a/lib/commons/matches/from-definition.js +++ b/lib/commons/matches/from-definition.js @@ -23,11 +23,11 @@ const matchers = ['nodeName', 'attributes', 'properties', 'condition']; * @returns {Boolean} */ matches.fromDefinition = function matchFromDefinition(node, definition) { - node = node.actualNode || node; if (Array.isArray(definition)) { return definition.some(definitionItem => matches(node, definitionItem)); } if (typeof definition === 'string') { + // TODO: what to do here? what strings are being passed? return axe.utils.matchesSelector(node, definition); } diff --git a/lib/commons/matches/node-name.js b/lib/commons/matches/node-name.js index 732f781088..4b022cce89 100644 --- a/lib/commons/matches/node-name.js +++ b/lib/commons/matches/node-name.js @@ -20,6 +20,7 @@ matches.nodeName = function matchNodeName(node, matcher, { isXHTML } = {}) { if (typeof isXHTML === 'undefined') { // When the matcher is a string, use native .matches() function: if (typeof matcher === 'string') { + // TODO: what to do about native matches()? use qsa matchers? return axe.utils.matchesSelector(node, matcher); } @@ -29,6 +30,9 @@ matches.nodeName = function matchNodeName(node, matcher, { isXHTML } = {}) { isXHTML = isXHTMLGlobal; } - const nodeName = isXHTML ? node.nodeName : node.nodeName.toLowerCase(); - return matches.fromPrimative(nodeName, matcher); + let vNode = + node instanceof axe.AbstractVirtualNode + ? node + : axe.utils.getNodeFromTree(node); + return matches.fromPrimative(vNode.props().nodeName, matcher); }; diff --git a/lib/commons/matches/properties.js b/lib/commons/matches/properties.js index c317706e6a..0b3747f453 100644 --- a/lib/commons/matches/properties.js +++ b/lib/commons/matches/properties.js @@ -19,7 +19,10 @@ * @returns {Boolean} */ matches.properties = function matchesProperties(node, matcher) { - node = node.actualNode || node; - const out = matches.fromFunction(propName => node[propName], matcher); - return out; + let vNode = + node instanceof axe.AbstractVirtualNode + ? node + : axe.utils.getNodeFromTree(node); + // TODO: not all props are on virtualNode.props() (e.g. value) + return matches.fromFunction(propName => vNode.props()[propName], matcher); }; From ea6009f653e84cb018ae83922921b2a70e10cefd Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Fri, 17 Jan 2020 08:09:26 -0700 Subject: [PATCH 2/7] [WIP] feat(matches, convert-selector, matchers): use virtual node for commons.matchers --- lib/commons/matches/attributes.js | 2 +- lib/commons/matches/from-definition.js | 12 +- lib/commons/matches/node-name.js | 25 +-- lib/commons/matches/properties.js | 5 +- lib/core/base/virtual-node/virtual-node.js | 14 +- lib/core/utils/convert-selector.js | 150 ++++++++++++++ lib/core/utils/matches.js | 108 +++++++++++ lib/core/utils/qsa.js | 215 +-------------------- test/commons/matches/attributes.js | 50 +++-- test/commons/matches/from-definition.js | 95 ++++----- test/commons/matches/node-name.js | 54 +++--- test/commons/matches/properties.js | 95 ++++----- 12 files changed, 424 insertions(+), 401 deletions(-) create mode 100644 lib/core/utils/convert-selector.js create mode 100644 lib/core/utils/matches.js diff --git a/lib/commons/matches/attributes.js b/lib/commons/matches/attributes.js index 93428089f1..81b1472962 100644 --- a/lib/commons/matches/attributes.js +++ b/lib/commons/matches/attributes.js @@ -18,7 +18,7 @@ * @returns {Boolean} */ matches.attributes = function matchesAttributes(node, matcher) { - let vNode = + const vNode = node instanceof axe.AbstractVirtualNode ? node : axe.utils.getNodeFromTree(node); diff --git a/lib/commons/matches/from-definition.js b/lib/commons/matches/from-definition.js index 0d67c2b4d8..020d25bbb1 100644 --- a/lib/commons/matches/from-definition.js +++ b/lib/commons/matches/from-definition.js @@ -23,12 +23,16 @@ const matchers = ['nodeName', 'attributes', 'properties', 'condition']; * @returns {Boolean} */ matches.fromDefinition = function matchFromDefinition(node, definition) { + const vNode = + node instanceof axe.AbstractVirtualNode + ? node + : axe.utils.getNodeFromTree(node); + if (Array.isArray(definition)) { - return definition.some(definitionItem => matches(node, definitionItem)); + return definition.some(definitionItem => matches(vNode, definitionItem)); } if (typeof definition === 'string') { - // TODO: what to do here? what strings are being passed? - return axe.utils.matchesSelector(node, definition); + return axe.utils.matches(vNode, definition); } return Object.keys(definition).every(matcherName => { @@ -42,6 +46,6 @@ matches.fromDefinition = function matchFromDefinition(node, definition) { // Find the matcher that goes into the matches method. // 'div', /^div$/, (str) => str === 'div', etc. const matcher = definition[matcherName]; - return matchMethod(node, matcher); + return matchMethod(vNode, matcher); }); }; diff --git a/lib/commons/matches/node-name.js b/lib/commons/matches/node-name.js index 4b022cce89..4c26083946 100644 --- a/lib/commons/matches/node-name.js +++ b/lib/commons/matches/node-name.js @@ -1,7 +1,6 @@ /* global matches */ -let isXHTMLGlobal; /** - * Check if the nodeName of a node matches some value + * Check if the nodeName of a node matches some value. * * Note: matches.nodeName(node, matcher) can be indirectly used through * matches(node, { nodeName: matcher }) @@ -11,28 +10,16 @@ let isXHTMLGlobal; * matches.nodeName(node, ['div', 'span']) * ``` * + * @deprecated HTMLElement is deprecated, use VirtualNode instead + * * @param {HTMLElement|VirtualNode} node * @param {Object} Attribute matcher * @returns {Boolean} */ -matches.nodeName = function matchNodeName(node, matcher, { isXHTML } = {}) { - node = node.actualNode || node; - if (typeof isXHTML === 'undefined') { - // When the matcher is a string, use native .matches() function: - if (typeof matcher === 'string') { - // TODO: what to do about native matches()? use qsa matchers? - return axe.utils.matchesSelector(node, matcher); - } - - if (typeof isXHTMLGlobal === 'undefined') { - isXHTMLGlobal = axe.utils.isXHTML(node.ownerDocument); - } - isXHTML = isXHTMLGlobal; - } - - let vNode = +matches.nodeName = function matchNodeName(node, matcher) { + const vNode = node instanceof axe.AbstractVirtualNode ? node : axe.utils.getNodeFromTree(node); - return matches.fromPrimative(vNode.props().nodeName, matcher); + return matches.fromPrimative(vNode.props.nodeName, matcher); }; diff --git a/lib/commons/matches/properties.js b/lib/commons/matches/properties.js index 0b3747f453..940463aaae 100644 --- a/lib/commons/matches/properties.js +++ b/lib/commons/matches/properties.js @@ -19,10 +19,9 @@ * @returns {Boolean} */ matches.properties = function matchesProperties(node, matcher) { - let vNode = + const vNode = node instanceof axe.AbstractVirtualNode ? node : axe.utils.getNodeFromTree(node); - // TODO: not all props are on virtualNode.props() (e.g. value) - return matches.fromFunction(propName => vNode.props()[propName], matcher); + return matches.fromFunction(propName => vNode.props[propName], matcher); }; diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index 835c76d7f0..fe62447fcd 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -1,3 +1,5 @@ +let isXHTMLGlobal; + // class is unused in the file... // eslint-disable-next-line no-unused-vars class VirtualNode extends axe.AbstractVirtualNode { @@ -17,6 +19,11 @@ class VirtualNode extends axe.AbstractVirtualNode { this._isHidden = null; // will be populated by axe.utils.isHidden this._cache = {}; + if (typeof isXHTMLGlobal === 'undefined') { + isXHTMLGlobal = axe.utils.isXHTML(node.ownerDocument); + } + this._isXHTML = isXHTMLGlobal; + if (axe._cache.get('nodeMap')) { axe._cache.get('nodeMap').set(node, this); } @@ -25,13 +32,14 @@ class VirtualNode extends axe.AbstractVirtualNode { // 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; + const { nodeType, nodeName, id, type, multiple } = this.actualNode; return { nodeType, - nodeName: nodeName.toLowerCase(), + nodeName: this._isXHTML ? nodeName : nodeName.toLowerCase(), id, - type + type, + multiple }; } diff --git a/lib/core/utils/convert-selector.js b/lib/core/utils/convert-selector.js new file mode 100644 index 0000000000..34c3d32b9d --- /dev/null +++ b/lib/core/utils/convert-selector.js @@ -0,0 +1,150 @@ +var escapeRegExp = (function() { + /*! Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan MIT License */ + var from = /(?=[\-\[\]{}()*+?.\\\^$|,#\s])/g; + var to = '\\'; + return function(string) { + return string.replace(from, to); + }; +})(); + +const reUnescape = /\\/g; +function convertAttributes(atts) { + /*! Credit Mootools Copyright Mootools, MIT License */ + if (!atts) { + return; + } + return atts.map(att => { + const attributeKey = att.name.replace(reUnescape, ''); + const attributeValue = (att.value || '').replace(reUnescape, ''); + let test, regexp; + + switch (att.operator) { + case '^=': + regexp = new RegExp('^' + escapeRegExp(attributeValue)); + break; + case '$=': + regexp = new RegExp(escapeRegExp(attributeValue) + '$'); + break; + case '~=': + regexp = new RegExp( + '(^|\\s)' + escapeRegExp(attributeValue) + '(\\s|$)' + ); + break; + case '|=': + regexp = new RegExp('^' + escapeRegExp(attributeValue) + '(-|$)'); + break; + case '=': + test = function(value) { + return attributeValue === value; + }; + break; + case '*=': + test = function(value) { + return value && value.includes(attributeValue); + }; + break; + case '!=': + test = function(value) { + return attributeValue !== value; + }; + break; + default: + test = function(value) { + return !!value; + }; + } + + if (attributeValue === '' && /^[*$^]=$/.test(att.operator)) { + test = function() { + return false; + }; + } + + if (!test) { + test = function(value) { + return value && regexp.test(value); + }; + } + return { + key: attributeKey, + value: attributeValue, + test: test + }; + }); +} + +function convertClasses(classes) { + if (!classes) { + return; + } + return classes.map(className => { + className = className.replace(reUnescape, ''); + + return { + value: className, + regexp: new RegExp('(^|\\s)' + escapeRegExp(className) + '(\\s|$)') + }; + }); +} + +function convertPseudos(pseudos) { + if (!pseudos) { + return; + } + return pseudos.map(p => { + var expressions; + + if (p.name === 'not') { + expressions = p.value; + expressions = expressions.selectors + ? expressions.selectors + : [expressions]; + expressions = convertExpressions(expressions); + } + return { + name: p.name, + expressions: expressions, + value: p.value + }; + }); +} + +/** + * convert the css-selector-parser format into the Slick format + * @private + * @param Array {Object} expressions + * @return Array {Object} + * + */ +function convertExpressions(expressions) { + return expressions.map(exp => { + var newExp = []; + var rule = exp.rule; + while (rule) { + /* eslint no-restricted-syntax: 0 */ + // `.tagName` is a property coming from the `CSSSelectorParser` library + newExp.push({ + tag: rule.tagName ? rule.tagName.toLowerCase() : '*', + combinator: rule.nestingOperator ? rule.nestingOperator : ' ', + id: rule.id, + attributes: convertAttributes(rule.attrs), + classes: convertClasses(rule.classNames), + pseudos: convertPseudos(rule.pseudos) + }); + rule = rule.rule; + } + return newExp; + }); +} + +/** + * Convert a CSS selector to the Slick format expression + * + * @param {String} selector CSS selector to convert + * @returns {Object[]} Array of Slick format expressions + */ +axe.utils.convertSelector = function convertSelector(selector) { + var expressions = axe.utils.cssParser.parse(selector); + expressions = expressions.selectors ? expressions.selectors : [expressions]; + return convertExpressions(expressions); +}; diff --git a/lib/core/utils/matches.js b/lib/core/utils/matches.js new file mode 100644 index 0000000000..1512a02f0b --- /dev/null +++ b/lib/core/utils/matches.js @@ -0,0 +1,108 @@ +function matchesTag(vNode, exp) { + return ( + vNode.props.nodeType === 1 && + (exp.tag === '*' || vNode.props.nodeName === exp.tag) + ); +} + +function matchesClasses(vNode, exp) { + return !exp.classes || exp.classes.every(cl => vNode.hasClass(cl.value)); +} + +function matchesAttributes(vNode, exp) { + return ( + !exp.attributes || + exp.attributes.every(att => { + var nodeAtt = vNode.attr(att.key); + return nodeAtt !== null && (!att.value || att.test(nodeAtt)); + }) + ); +} + +function matchesId(vNode, exp) { + return !exp.id || vNode.props.id === exp.id; +} + +function matchesPseudos(target, exp) { + if ( + !exp.pseudos || + exp.pseudos.every(pseudo => { + if (pseudo.name === 'not') { + return !axe.utils.matchesExpression(target, pseudo.expressions[0]); + } + throw new Error( + 'the pseudo selector ' + pseudo.name + ' has not yet been implemented' + ); + }) + ) { + return true; + } + return false; +} + +function matchExpression(vNode, expression) { + return ( + matchesTag(vNode, expression) && + matchesClasses(vNode, expression) && + matchesAttributes(vNode, expression) && + matchesId(vNode, expression) && + matchesPseudos(vNode, expression) + ); +} + +/** + * Determine if a virtual node matches a Slick format CSS expression + * + * @method matchesExpression + * @memberof axe.utils + * @param {VirtualNode} vNode VirtualNode to match + * @param {Object|Object[]} expressions CSS selector expression or array of expressions + * @returns {Boolean} + */ +axe.utils.matchesExpression = function matchesExpression( + vNode, + expressions, + matchAnyParent +) { + let exps = [].concat(expressions); + let expression = exps.pop(); + let matches = matchExpression(vNode, expression); + + while (!matches && matchAnyParent && vNode.parent) { + vNode = vNode.parent; + matches = matchExpression(vNode, expression); + } + + if (exps.length) { + if ([' ', '>'].includes(expression.combinator) === false) { + throw new Error( + 'axe.utils.matchesExpression does not support the combinator: ' + + expression.combinator + ); + } + + matches = + matches && + axe.utils.matchesExpression( + vNode.parent, + exps, + expression.combinator === ' ' + ); + } + + return matches; +}; + +/** + * matches implementation that operates on a VirtualNode + * + * @method matches + * @memberof axe.utils + * @param {VirtualNode} vNode VirtualNode to match + * @param {String} selector CSS selector string + * @return {Boolean} + */ +axe.utils.matches = function matches(vNode, selector) { + let expressions = axe.utils.convertSelector(selector); + return axe.utils.matchesExpression(vNode, expressions[0]); +}; diff --git a/lib/core/utils/qsa.js b/lib/core/utils/qsa.js index e63c2f10a7..5957066999 100644 --- a/lib/core/utils/qsa.js +++ b/lib/core/utils/qsa.js @@ -1,191 +1,3 @@ -// The lines below is because the latedef option does not work -var convertExpressions = function() {}; -var matchExpressions = function() {}; - -// todo: implement an option to follow aria-owns - -function matchesTag(vNode, exp) { - return ( - vNode.props.nodeType === 1 && - (exp.tag === '*' || vNode.props.nodeName === exp.tag) - ); -} - -function matchesClasses(vNode, exp) { - return !exp.classes || exp.classes.every(cl => vNode.hasClass(cl.value)); -} - -function matchesAttributes(vNode, exp) { - return ( - !exp.attributes || - exp.attributes.every(att => { - var nodeAtt = vNode.attr(att.key); - return nodeAtt !== null && (!att.value || att.test(nodeAtt)); - }) - ); -} - -function matchesId(vNode, exp) { - return !exp.id || vNode.props.id === exp.id; -} - -function matchesPseudos(target, exp) { - if ( - !exp.pseudos || - exp.pseudos.every(pseudo => { - if (pseudo.name === 'not') { - return !matchExpressions([target], pseudo.expressions, false).length; - } - throw new Error( - 'the pseudo selector ' + pseudo.name + ' has not yet been implemented' - ); - }) - ) { - return true; - } - return false; -} - -var escapeRegExp = (function() { - /*! Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan MIT License */ - var from = /(?=[\-\[\]{}()*+?.\\\^$|,#\s])/g; - var to = '\\'; - return function(string) { - return string.replace(from, to); - }; -})(); - -var reUnescape = /\\/g; - -function convertAttributes(atts) { - /*! Credit Mootools Copyright Mootools, MIT License */ - if (!atts) { - return; - } - return atts.map(att => { - var attributeKey = att.name.replace(reUnescape, ''); - var attributeValue = (att.value || '').replace(reUnescape, ''); - var test, regexp; - - switch (att.operator) { - case '^=': - regexp = new RegExp('^' + escapeRegExp(attributeValue)); - break; - case '$=': - regexp = new RegExp(escapeRegExp(attributeValue) + '$'); - break; - case '~=': - regexp = new RegExp( - '(^|\\s)' + escapeRegExp(attributeValue) + '(\\s|$)' - ); - break; - case '|=': - regexp = new RegExp('^' + escapeRegExp(attributeValue) + '(-|$)'); - break; - case '=': - test = function(value) { - return attributeValue === value; - }; - break; - case '*=': - test = function(value) { - return value && value.includes(attributeValue); - }; - break; - case '!=': - test = function(value) { - return attributeValue !== value; - }; - break; - default: - test = function(value) { - return !!value; - }; - } - - if (attributeValue === '' && /^[*$^]=$/.test(att.operator)) { - test = function() { - return false; - }; - } - - if (!test) { - test = function(value) { - return value && regexp.test(value); - }; - } - return { - key: attributeKey, - value: attributeValue, - test: test - }; - }); -} - -function convertClasses(classes) { - if (!classes) { - return; - } - return classes.map(className => { - className = className.replace(reUnescape, ''); - - return { - value: className, - regexp: new RegExp('(^|\\s)' + escapeRegExp(className) + '(\\s|$)') - }; - }); -} - -function convertPseudos(pseudos) { - if (!pseudos) { - return; - } - return pseudos.map(p => { - var expressions; - - if (p.name === 'not') { - expressions = p.value; - expressions = expressions.selectors - ? expressions.selectors - : [expressions]; - expressions = convertExpressions(expressions); - } - return { - name: p.name, - expressions: expressions, - value: p.value - }; - }); -} - -/** - * convert the css-selector-parser format into the Slick format - * @private - * @param Array {Object} expressions - * @return Array {Object} - * - */ -convertExpressions = function(expressions) { - return expressions.map(exp => { - var newExp = []; - var rule = exp.rule; - while (rule) { - /* eslint no-restricted-syntax: 0 */ - // `.tagName` is a property coming from the `CSSSelectorParser` library - newExp.push({ - tag: rule.tagName ? rule.tagName.toLowerCase() : '*', - combinator: rule.nestingOperator ? rule.nestingOperator : ' ', - id: rule.id, - attributes: convertAttributes(rule.attrs), - classes: convertClasses(rule.classNames), - pseudos: convertPseudos(rule.pseudos) - }); - rule = rule.rule; - } - return newExp; - }); -}; - function createLocalVariables(vNodes, anyLevel, thisLevel, parentShadowId) { let retVal = { vNodes: vNodes.slice(), @@ -197,17 +9,7 @@ function createLocalVariables(vNodes, anyLevel, thisLevel, parentShadowId) { return retVal; } -function matchesSelector(vNode, exp) { - return ( - matchesTag(vNode, exp[0]) && - matchesClasses(vNode, exp[0]) && - matchesAttributes(vNode, exp[0]) && - matchesId(vNode, exp[0]) && - matchesPseudos(vNode, exp[0]) - ); -} - -matchExpressions = function(domTree, expressions, recurse, filter) { +function matchExpressions(domTree, expressions, filter) { let stack = []; let vNodes = Array.isArray(domTree) ? domTree : [domTree]; let currentLevel = createLocalVariables( @@ -229,7 +31,7 @@ matchExpressions = function(domTree, expressions, recurse, filter) { let exp = combined[i]; if ( (!exp[0].id || vNode.shadowId === currentLevel.parentShadowId) && - matchesSelector(vNode, exp) + axe.utils.matchesExpression(vNode, exp[0]) ) { if (exp.length === 1) { if (!added && (!filter || filter(vNode))) { @@ -260,8 +62,8 @@ matchExpressions = function(domTree, expressions, recurse, filter) { childAny.push(exp); } } - // "recurse" - if (vNode.children && vNode.children.length && recurse) { + + if (vNode.children && vNode.children.length) { stack.push(currentLevel); currentLevel = createLocalVariables( vNode.children, @@ -276,7 +78,7 @@ matchExpressions = function(domTree, expressions, recurse, filter) { } } return result; -}; +} /** * querySelectorAll implementation that operates on the flattened tree (supports shadow DOM) @@ -301,11 +103,8 @@ axe.utils.querySelectorAll = function(domTree, selector) { * @param {Function} filter function (optional) * @return {Array} Elements matched by any of the selectors and filtered by the filter function */ - axe.utils.querySelectorAllFilter = function(domTree, selector, filter) { domTree = Array.isArray(domTree) ? domTree : [domTree]; - var expressions = axe.utils.cssParser.parse(selector); - expressions = expressions.selectors ? expressions.selectors : [expressions]; - expressions = convertExpressions(expressions); - return matchExpressions(domTree, expressions, true, filter); + const expressions = axe.utils.convertSelector(selector); + return matchExpressions(domTree, expressions, filter); }; diff --git a/test/commons/matches/attributes.js b/test/commons/matches/attributes.js index a60671d87b..6ee32b1459 100644 --- a/test/commons/matches/attributes.js +++ b/test/commons/matches/attributes.js @@ -1,14 +1,18 @@ describe('matches.attributes', function() { var attributes = axe.commons.matches.attributes; var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + beforeEach(function() { fixture.innerHTML = ''; }); it('returns true if all attributes match', function() { - fixture.innerHTML = ''; + var virtualNode = queryFixture( + '' + ); assert.isTrue( - attributes(fixture.firstChild, { + attributes(virtualNode, { foo: 'baz', bar: 'foo', baz: 'bar' @@ -17,9 +21,11 @@ describe('matches.attributes', function() { }); it('returns false if some attributes do not match', function() { - fixture.innerHTML = ''; + var virtualNode = queryFixture( + '' + ); assert.isFalse( - attributes(fixture.firstChild, { + attributes(virtualNode, { foo: 'baz', bar: 'foo', baz: 'baz' @@ -28,9 +34,11 @@ describe('matches.attributes', function() { }); it('returns false if any attributes are missing', function() { - fixture.innerHTML = ''; + var virtualNode = queryFixture( + '' + ); assert.isFalse( - attributes(fixture.firstChild, { + attributes(virtualNode, { foo: 'baz', bar: 'foo', baz: 'bar' @@ -38,28 +46,16 @@ describe('matches.attributes', function() { ); }); - it('works with virtual nodes', function() { - fixture.innerHTML = ''; - assert.isTrue( - attributes( - { - actualNode: fixture.firstChild - }, - { - foo: 'bar', - bar: 'foo' - } - ) + it('works with actual nodes', function() { + var virtualNode = queryFixture( + '' ); - assert.isFalse( - attributes( - { - actualNode: fixture.firstChild - }, - { - baz: 'baz' - } - ) + assert.isTrue( + attributes(virtualNode.actualNode, { + foo: 'baz', + bar: 'foo', + baz: 'bar' + }) ); }); }); diff --git a/test/commons/matches/from-definition.js b/test/commons/matches/from-definition.js index 969c9aec90..52b4f033c4 100644 --- a/test/commons/matches/from-definition.js +++ b/test/commons/matches/from-definition.js @@ -1,18 +1,20 @@ describe('matches.fromDefinition', function() { var fromDefinition = axe.commons.matches.fromDefinition; var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + beforeEach(function() { fixture.innerHTML = ''; }); it('applies a css selector when the matcher is a string', function() { - fixture.innerHTML = '
foo
'; - assert.isTrue(fromDefinition(fixture.firstChild, '#fixture > div')); - assert.isFalse(fromDefinition(fixture.firstChild, '#fixture > span')); + var virtualNode = queryFixture('
foo
'); + assert.isTrue(fromDefinition(virtualNode, '#fixture > div')); + assert.isFalse(fromDefinition(virtualNode, '#fixture > span')); }); it('matches a definition with a `nodeName` property', function() { - fixture.innerHTML = '
foo
'; + var virtualNode = queryFixture('
foo
'); var matchers = [ 'div', ['div', 'span'], @@ -23,20 +25,20 @@ describe('matches.fromDefinition', function() { ]; matchers.forEach(function(matcher) { assert.isTrue( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { nodeName: matcher }) ); }); assert.isFalse( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { nodeName: 'span' }) ); }); it('matches a definition with an `attributes` property', function() { - fixture.innerHTML = '
foo
'; + var virtualNode = queryFixture('
foo
'); var matchers = [ 'bar', ['bar', 'baz'], @@ -47,7 +49,7 @@ describe('matches.fromDefinition', function() { ]; matchers.forEach(function(matcher) { assert.isTrue( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { attributes: { foo: matcher } @@ -55,7 +57,7 @@ describe('matches.fromDefinition', function() { ); }); assert.isFalse( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { attributes: { foo: 'baz' } @@ -64,7 +66,7 @@ describe('matches.fromDefinition', function() { }); it('matches a definition with a `properties` property', function() { - fixture.innerHTML = ''; + var virtualNode = queryFixture(''); var matchers = [ 'text', ['text', 'password'], @@ -75,7 +77,7 @@ describe('matches.fromDefinition', function() { ]; matchers.forEach(function(matcher) { assert.isTrue( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { properties: { type: matcher } @@ -83,7 +85,7 @@ describe('matches.fromDefinition', function() { ); }); assert.isFalse( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { properties: { type: 'password' } @@ -92,13 +94,15 @@ describe('matches.fromDefinition', function() { }); it('returns true when all matching properties return true', function() { - fixture.innerHTML = ''; + var virtualNode = queryFixture( + '' + ); assert.isTrue( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { nodeName: 'input', properties: { type: 'text', - value: 'bar' + id: 'target' }, attributes: { 'aria-disabled': 'true' @@ -108,9 +112,11 @@ describe('matches.fromDefinition', function() { }); it('returns false when some matching properties return false', function() { - fixture.innerHTML = ''; + var virtualNode = queryFixture( + '' + ); assert.isFalse( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { nodeName: 'input', attributes: { 'aria-disabled': 'false' @@ -119,41 +125,38 @@ describe('matches.fromDefinition', function() { ); }); - describe('with virtual nodes', function() { + describe('with actual nodes', function() { it('matches using a string', function() { - fixture.innerHTML = '
foo
'; - var node = { actualNode: fixture.firstChild }; - assert.isTrue(fromDefinition(node, 'div')); - assert.isFalse(fromDefinition(node, 'span')); + var virtualNode = queryFixture('
foo
'); + assert.isTrue(fromDefinition(virtualNode.actualNode, 'div')); + assert.isFalse(fromDefinition(virtualNode.actualNode, 'span')); }); it('matches nodeName', function() { - fixture.innerHTML = '
foo
'; - var node = { actualNode: fixture.firstChild }; + var virtualNode = queryFixture('
foo
'); assert.isTrue( - fromDefinition(node, { + fromDefinition(virtualNode.actualNode, { nodeName: 'div' }) ); assert.isFalse( - fromDefinition(node, { + fromDefinition(virtualNode.actualNode, { nodeName: 'span' }) ); }); it('matches attributes', function() { - fixture.innerHTML = '
foo
'; - var node = { actualNode: fixture.firstChild }; + var virtualNode = queryFixture('
foo
'); assert.isTrue( - fromDefinition(node, { + fromDefinition(virtualNode.actualNode, { attributes: { foo: 'bar' } }) ); assert.isFalse( - fromDefinition(node, { + fromDefinition(virtualNode.actualNode, { attributes: { foo: 'baz' } @@ -162,19 +165,18 @@ describe('matches.fromDefinition', function() { }); it('matches properties', function() { - fixture.innerHTML = ''; - var node = { actualNode: fixture.firstChild }; + var virtualNode = queryFixture(''); assert.isTrue( - fromDefinition(node, { + fromDefinition(virtualNode.actualNode, { properties: { - value: 'foo' + id: 'target' } }) ); assert.isFalse( - fromDefinition(node, { + fromDefinition(virtualNode.actualNode, { properties: { - value: 'bar' + id: 'bar' } }) ); @@ -183,24 +185,25 @@ describe('matches.fromDefinition', function() { describe('with a `condition` property', function() { it('calls condition and uses its return value as a matcher', function() { - fixture.innerHTML = '
foo
'; + var virtualNode = queryFixture('
foo
'); + var called = false; assert.isTrue( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { condition: function(node) { - assert.deepEqual(node, fixture.firstChild); - node.setAttribute('foo', 'bar'); + assert.deepEqual(node, virtualNode); + called = true; return true; } }) ); assert.isFalse( - fromDefinition(fixture.firstChild, { + fromDefinition(virtualNode, { condition: function() { return false; } }) ); - assert.equal(fixture.firstChild.getAttribute('foo'), 'bar'); + assert.isTrue(called); }); it('uses the return value as a matcher', function() { @@ -225,9 +228,9 @@ describe('matches.fromDefinition', function() { describe('with an `array` of definitions', function() { it('returns true if any definition in the array matches', function() { - fixture.innerHTML = '
foo
'; + var virtualNode = queryFixture('
foo
'); assert.isTrue( - fromDefinition(fixture.firstChild, [ + fromDefinition(virtualNode, [ { nodeName: 'span' }, { nodeName: 'div' }, { nodeName: 'h1' } @@ -236,9 +239,9 @@ describe('matches.fromDefinition', function() { }); it('returns false if none definition in the array matches', function() { - fixture.innerHTML = ''; + var virtualNode = queryFixture(''); assert.isFalse( - fromDefinition(fixture.firstChild, [ + fromDefinition(virtualNode, [ { nodeName: 'span' }, { nodeName: 'div' }, { nodeName: 'h1' } diff --git a/test/commons/matches/node-name.js b/test/commons/matches/node-name.js index e82d861dd8..7f136ccecb 100644 --- a/test/commons/matches/node-name.js +++ b/test/commons/matches/node-name.js @@ -1,68 +1,62 @@ describe('matches.nodeName', function() { var matchNodeName = axe.commons.matches.nodeName; var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + beforeEach(function() { fixture.innerHTML = ''; }); it('returns true if the nodeName is the same as the matcher', function() { - fixture.innerHTML = '

foo

'; - assert.isTrue(matchNodeName(fixture.firstChild, 'h1')); + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matchNodeName(virtualNode, 'h1')); }); it('returns true if the nodename is included in an array', function() { - fixture.innerHTML = '

foo

'; - assert.isTrue(matchNodeName(fixture.firstChild, ['h3', 'h2', 'h1'])); + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matchNodeName(virtualNode, ['h3', 'h2', 'h1'])); }); it('returns true if the nodeName matches a regexp', function() { - fixture.innerHTML = '

foo

'; - assert.isTrue(matchNodeName(fixture.firstChild, /^h[0-6]$/)); + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matchNodeName(virtualNode, /^h[0-6]$/)); }); it('returns true if the nodeName matches with a function', function() { - fixture.innerHTML = '

foo

'; + var virtualNode = queryFixture('

foo

'); assert.isTrue( - matchNodeName(fixture.firstChild, function(nodeName) { + matchNodeName(virtualNode, function(nodeName) { return nodeName === 'h1'; }) ); }); it('returns false if the nodeName does not match', function() { - fixture.innerHTML = '
foo
'; - assert.isFalse(matchNodeName(fixture.firstChild, 'h1')); - assert.isFalse(matchNodeName(fixture.firstChild, ['h3', 'h2', 'h1'])); - assert.isFalse(matchNodeName(fixture.firstChild, /^h[0-6]$/)); + var virtualNode = queryFixture('
foo
'); + assert.isFalse(matchNodeName(virtualNode, 'h1')); + assert.isFalse(matchNodeName(virtualNode, ['h3', 'h2', 'h1'])); + assert.isFalse(matchNodeName(virtualNode, /^h[0-6]$/)); assert.isFalse( - matchNodeName(fixture.firstChild, function(nodeName) { + matchNodeName(virtualNode, function(nodeName) { return nodeName === 'h1'; }) ); }); it('is case sensitive for XHTML', function() { - var elm = { - // Mock DOM node - nodeName: 'H1', - ownerDocument: document - }; - assert.isFalse(matchNodeName(elm, 'h1', { isXHTML: true })); + var virtualNode = queryFixture('

foo

'); + virtualNode._isXHTML = true; + assert.isFalse(matchNodeName(virtualNode, 'h1')); }); it('is case insensitive for HTML, but not for XHTML', function() { - var elm = { - // Mock DOM node - nodeName: 'H1', - ownerDocument: document - }; - assert.isTrue(matchNodeName(elm, 'h1', { isXHTML: false })); + var virtualNode = queryFixture('

foo

'); + virtualNode._isXHTML = true; + assert.isFalse(matchNodeName(virtualNode, 'h1')); }); - it('works with virtual nodes', function() { - fixture.innerHTML = '

foo

'; - var virtualNode = { actualNode: fixture.firstChild }; - assert.isTrue(matchNodeName(virtualNode, 'h1')); - assert.isFalse(matchNodeName(virtualNode, 'div')); + it('works with actual nodes', function() { + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matchNodeName(virtualNode.actual, 'h1')); }); }); diff --git a/test/commons/matches/properties.js b/test/commons/matches/properties.js index 0e5fbd0e3b..086ec15dd6 100644 --- a/test/commons/matches/properties.js +++ b/test/commons/matches/properties.js @@ -1,82 +1,57 @@ describe('matches.properties', function() { var properties = axe.commons.matches.properties; + var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + + beforeEach(function() { + fixture.innerHTML = ''; + }); it('returns true if all properties match', function() { + var virtualNode = queryFixture(''); + assert.isTrue( - properties( - { - foo: 'baz', - bar: 'foo', - baz: 'bar' - }, - { - foo: 'baz', - baz: 'bar' - } - ) + properties(virtualNode, { + nodeName: 'input', + id: 'target', + type: 'text' + }) ); }); it('returns false if some properties do not match', function() { + var virtualNode = queryFixture(''); + assert.isFalse( - properties( - { - foo: 'baz', - bar: 'foo', - baz: 'bar' - }, - { - foo: 'baz', - bar: 'foo', - baz: 'baz' - } - ) + properties(virtualNode, { + nodeName: 'input', + id: 'target', + type: 'num' + }) ); }); it('returns false if any properties are missing', function() { + var virtualNode = queryFixture('

foo

'); + assert.isFalse( - properties( - { - foo: 'baz', - baz: 'bar' - }, - { - foo: 'baz', - bar: 'foo', - baz: 'bar' - } - ) + properties(virtualNode, { + nodeName: 'h1', + id: 'target', + type: 'text' + }) ); }); - it('works with virtual nodes', function() { + it('works with actual nodes', function() { + var virtualNode = queryFixture(''); + assert.isTrue( - properties( - { - actualNode: { - foo: 'bar', - bar: 'foo' - } - }, - { - foo: 'bar', - bar: 'foo' - } - ) - ); - assert.isFalse( - properties( - { - actualNode: { - foo: 'bar', - bar: 'foo' - } - }, - { - baz: 'baz' - } - ) + properties(virtualNode.actualNode, { + nodeName: 'input', + id: 'target', + type: 'text' + }) ); }); }); From c4092efa61c517346d36408463fc8d8aac549099 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Fri, 17 Jan 2020 11:12:20 -0700 Subject: [PATCH 3/7] add tests for matches --- lib/core/utils/convert-selector.js | 1 + lib/core/utils/matches.js | 1 + test/commons/matches/node-name.js | 2 +- test/core/utils/matches.js | 232 +++++++++++++++++++++++++++++ 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 test/core/utils/matches.js diff --git a/lib/core/utils/convert-selector.js b/lib/core/utils/convert-selector.js index 34c3d32b9d..62b08b42bd 100644 --- a/lib/core/utils/convert-selector.js +++ b/lib/core/utils/convert-selector.js @@ -140,6 +140,7 @@ function convertExpressions(expressions) { /** * Convert a CSS selector to the Slick format expression * + * @private * @param {String} selector CSS selector to convert * @returns {Object[]} Array of Slick format expressions */ diff --git a/lib/core/utils/matches.js b/lib/core/utils/matches.js index 1512a02f0b..7bc9f0955b 100644 --- a/lib/core/utils/matches.js +++ b/lib/core/utils/matches.js @@ -53,6 +53,7 @@ function matchExpression(vNode, expression) { /** * Determine if a virtual node matches a Slick format CSS expression * + * @private * @method matchesExpression * @memberof axe.utils * @param {VirtualNode} vNode VirtualNode to match diff --git a/test/commons/matches/node-name.js b/test/commons/matches/node-name.js index 7f136ccecb..0c3d29e34f 100644 --- a/test/commons/matches/node-name.js +++ b/test/commons/matches/node-name.js @@ -57,6 +57,6 @@ describe('matches.nodeName', function() { it('works with actual nodes', function() { var virtualNode = queryFixture('

foo

'); - assert.isTrue(matchNodeName(virtualNode.actual, 'h1')); + assert.isTrue(matchNodeName(virtualNode.actualNode, 'h1')); }); }); diff --git a/test/core/utils/matches.js b/test/core/utils/matches.js new file mode 100644 index 0000000000..51cdc9ed10 --- /dev/null +++ b/test/core/utils/matches.js @@ -0,0 +1,232 @@ +describe.only('utils.matches', function() { + var matches = axe.utils.matches; + var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + + afterEach(function() { + fixture.innerHTML = ''; + }); + + describe('tag', function() { + it('returns true if tag matches', function() { + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matches(virtualNode, 'h1')); + }); + + it('returns false if the tag does not match', function() { + var virtualNode = queryFixture('

foo

'); + assert.isFalse(matches(virtualNode, 'div')); + }); + + it('is case sensitive for XHTML', function() { + var virtualNode = queryFixture('

foo

'); + virtualNode._isXHTML = true; + assert.isFalse(matches(virtualNode, 'h1')); + }); + + it('is case insensitive for HTML, but not for XHTML', function() { + var virtualNode = queryFixture('

foo

'); + virtualNode._isXHTML = true; + assert.isFalse(matches(virtualNode, 'h1')); + }); + }); + + describe('classes', function() { + it('returns true if all classes match', function() { + var virtualNode = queryFixture( + '' + ); + assert.isTrue(matches(virtualNode, '.foo.bar.baz')); + }); + + it('returns false if some classes do not match', function() { + var virtualNode = queryFixture( + '' + ); + assert.isFalse(matches(virtualNode, '.foo.bar.bazz')); + }); + + it('returns false if any classes are missing', function() { + var virtualNode = queryFixture( + '' + ); + assert.isFalse(matches(virtualNode, '.foo.bar.baz')); + }); + }); + + describe('attributes', function() { + it('returns true if attribute exists', function() { + var virtualNode = queryFixture( + '' + ); + assert.isTrue(matches(virtualNode, '[foo]')); + }); + + it('returns true if attribute matches', function() { + var virtualNode = queryFixture( + '' + ); + assert.isTrue(matches(virtualNode, '[foo=baz]')); + }); + + it('returns true if all attributes match', function() { + var virtualNode = queryFixture( + '' + ); + assert.isTrue(matches(virtualNode, '[foo="baz"][bar="foo"][baz="bar"]')); + }); + + it('returns false if some attributes do not match', function() { + var virtualNode = queryFixture( + '' + ); + assert.isFalse(matches(virtualNode, '[foo="baz"][bar="foo"][baz="baz"]')); + }); + + it('returns false if any attributes are missing', function() { + var virtualNode = queryFixture( + '' + ); + assert.isFalse(matches(virtualNode, '[foo="baz"][bar="foo"][baz="bar"]')); + }); + }); + + describe('id', function() { + it('returns true if id matches', function() { + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matches(virtualNode, '#target')); + }); + + it('returns false if the id does not match', function() { + var virtualNode = queryFixture('

foo

'); + assert.isFalse(matches(virtualNode, '#notTarget')); + }); + }); + + describe('pseudos', function() { + it('returns true if :not matches using tag', function() { + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matches(virtualNode, 'h1:not(span)')); + }); + + it('returns true if :not matches using class', function() { + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matches(virtualNode, 'h1:not(.foo)')); + }); + + it('returns true if :not matches using attribute', function() { + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matches(virtualNode, 'h1:not([class])')); + }); + + it('returns true if :not matches using id', function() { + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matches(virtualNode, 'h1:not(#foo)')); + }); + + it('returns false if :not matches element', function() { + var virtualNode = queryFixture('

foo

'); + assert.isFalse(matches(virtualNode, 'h1:not([id])')); + }); + + it('throws error if pseudo is not implemented', function() { + var virtualNode = queryFixture('

foo

'); + assert.throws(function() { + matches(virtualNode, 'h1:empty'); + }); + assert.throws(function() { + matches(virtualNode, 'h1::before'); + }); + }); + }); + + describe('complex', function() { + it('returns true for complex selector', function() { + var virtualNode = queryFixture( + '' + ); + assert.isTrue(matches(virtualNode, 'span.foo[id="target"]:not(div)')); + }); + + it('returns false if any part of the selector does not match', function() { + var virtualNode = queryFixture( + '' + ); + assert.isFalse(matches(virtualNode, 'span.foo[id="target"]:not(span)')); + }); + }); + + describe('combinator', function() { + it('returns true if parent selector matches', function() { + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matches(virtualNode, 'div > h1')); + }); + + it('returns true if nested parent selector matches', function() { + var virtualNode = queryFixture( + '

foo

' + ); + assert.isTrue(matches(virtualNode, 'main > div > h1')); + }); + + it('returns true if hierarchical selector matches', function() { + var virtualNode = queryFixture('

foo

'); + assert.isTrue(matches(virtualNode, 'div h1')); + }); + + it('returns true if nested hierarchical selector matches', function() { + var virtualNode = queryFixture( + '

foo

' + ); + assert.isTrue(matches(virtualNode, 'div tr h1')); + }); + + it('returns true if mixed parent and hierarchical selector matches', function() { + var virtualNode = queryFixture( + '

foo

' + ); + assert.isTrue(matches(virtualNode, 'div tr > td h1')); + }); + + it('returns false if parent selector does not match', function() { + var virtualNode = queryFixture('

foo

'); + assert.isFalse(matches(virtualNode, 'span > h1')); + }); + + it('returns false if nested parent selector does not match', function() { + var virtualNode = queryFixture( + '

foo

' + ); + assert.isFalse(matches(virtualNode, 'span > div > h1')); + }); + + it('returns false if hierarchical selector does not match', function() { + var virtualNode = queryFixture('

foo

'); + assert.isFalse(matches(virtualNode, 'span h1')); + }); + + it('returns false if nested hierarchical selector does not match', function() { + var virtualNode = queryFixture( + '

foo

' + ); + assert.isFalse(matches(virtualNode, 'div span h1')); + }); + + it('returns false if mixed parent and hierarchical selector does not match', function() { + var virtualNode = queryFixture( + '

foo

' + ); + assert.isFalse(matches(virtualNode, 'div span > td h1')); + }); + + it('throws error if combinator is not implemented', function() { + var virtualNode = queryFixture('

foo

'); + assert.throws(function() { + matches(virtualNode, 'div + h1'); + }); + assert.throws(function() { + matches(virtualNode, 'div ~ h1'); + }); + }); + }); +}); From 818d1f28841cd309f612a9111947452cc0cba43e Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Fri, 17 Jan 2020 11:16:10 -0700 Subject: [PATCH 4/7] refactor --- lib/core/utils/convert-selector.js | 151 ---------------------------- lib/core/utils/matches.js | 152 +++++++++++++++++++++++++++++ test/core/utils/matches.js | 2 +- 3 files changed, 153 insertions(+), 152 deletions(-) delete mode 100644 lib/core/utils/convert-selector.js diff --git a/lib/core/utils/convert-selector.js b/lib/core/utils/convert-selector.js deleted file mode 100644 index 62b08b42bd..0000000000 --- a/lib/core/utils/convert-selector.js +++ /dev/null @@ -1,151 +0,0 @@ -var escapeRegExp = (function() { - /*! Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan MIT License */ - var from = /(?=[\-\[\]{}()*+?.\\\^$|,#\s])/g; - var to = '\\'; - return function(string) { - return string.replace(from, to); - }; -})(); - -const reUnescape = /\\/g; -function convertAttributes(atts) { - /*! Credit Mootools Copyright Mootools, MIT License */ - if (!atts) { - return; - } - return atts.map(att => { - const attributeKey = att.name.replace(reUnescape, ''); - const attributeValue = (att.value || '').replace(reUnescape, ''); - let test, regexp; - - switch (att.operator) { - case '^=': - regexp = new RegExp('^' + escapeRegExp(attributeValue)); - break; - case '$=': - regexp = new RegExp(escapeRegExp(attributeValue) + '$'); - break; - case '~=': - regexp = new RegExp( - '(^|\\s)' + escapeRegExp(attributeValue) + '(\\s|$)' - ); - break; - case '|=': - regexp = new RegExp('^' + escapeRegExp(attributeValue) + '(-|$)'); - break; - case '=': - test = function(value) { - return attributeValue === value; - }; - break; - case '*=': - test = function(value) { - return value && value.includes(attributeValue); - }; - break; - case '!=': - test = function(value) { - return attributeValue !== value; - }; - break; - default: - test = function(value) { - return !!value; - }; - } - - if (attributeValue === '' && /^[*$^]=$/.test(att.operator)) { - test = function() { - return false; - }; - } - - if (!test) { - test = function(value) { - return value && regexp.test(value); - }; - } - return { - key: attributeKey, - value: attributeValue, - test: test - }; - }); -} - -function convertClasses(classes) { - if (!classes) { - return; - } - return classes.map(className => { - className = className.replace(reUnescape, ''); - - return { - value: className, - regexp: new RegExp('(^|\\s)' + escapeRegExp(className) + '(\\s|$)') - }; - }); -} - -function convertPseudos(pseudos) { - if (!pseudos) { - return; - } - return pseudos.map(p => { - var expressions; - - if (p.name === 'not') { - expressions = p.value; - expressions = expressions.selectors - ? expressions.selectors - : [expressions]; - expressions = convertExpressions(expressions); - } - return { - name: p.name, - expressions: expressions, - value: p.value - }; - }); -} - -/** - * convert the css-selector-parser format into the Slick format - * @private - * @param Array {Object} expressions - * @return Array {Object} - * - */ -function convertExpressions(expressions) { - return expressions.map(exp => { - var newExp = []; - var rule = exp.rule; - while (rule) { - /* eslint no-restricted-syntax: 0 */ - // `.tagName` is a property coming from the `CSSSelectorParser` library - newExp.push({ - tag: rule.tagName ? rule.tagName.toLowerCase() : '*', - combinator: rule.nestingOperator ? rule.nestingOperator : ' ', - id: rule.id, - attributes: convertAttributes(rule.attrs), - classes: convertClasses(rule.classNames), - pseudos: convertPseudos(rule.pseudos) - }); - rule = rule.rule; - } - return newExp; - }); -} - -/** - * Convert a CSS selector to the Slick format expression - * - * @private - * @param {String} selector CSS selector to convert - * @returns {Object[]} Array of Slick format expressions - */ -axe.utils.convertSelector = function convertSelector(selector) { - var expressions = axe.utils.cssParser.parse(selector); - expressions = expressions.selectors ? expressions.selectors : [expressions]; - return convertExpressions(expressions); -}; diff --git a/lib/core/utils/matches.js b/lib/core/utils/matches.js index 7bc9f0955b..a0a4e28cc0 100644 --- a/lib/core/utils/matches.js +++ b/lib/core/utils/matches.js @@ -50,6 +50,158 @@ function matchExpression(vNode, expression) { ); } +var escapeRegExp = (function() { + /*! Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan MIT License */ + var from = /(?=[\-\[\]{}()*+?.\\\^$|,#\s])/g; + var to = '\\'; + return function(string) { + return string.replace(from, to); + }; +})(); + +const reUnescape = /\\/g; +function convertAttributes(atts) { + /*! Credit Mootools Copyright Mootools, MIT License */ + if (!atts) { + return; + } + return atts.map(att => { + const attributeKey = att.name.replace(reUnescape, ''); + const attributeValue = (att.value || '').replace(reUnescape, ''); + let test, regexp; + + switch (att.operator) { + case '^=': + regexp = new RegExp('^' + escapeRegExp(attributeValue)); + break; + case '$=': + regexp = new RegExp(escapeRegExp(attributeValue) + '$'); + break; + case '~=': + regexp = new RegExp( + '(^|\\s)' + escapeRegExp(attributeValue) + '(\\s|$)' + ); + break; + case '|=': + regexp = new RegExp('^' + escapeRegExp(attributeValue) + '(-|$)'); + break; + case '=': + test = function(value) { + return attributeValue === value; + }; + break; + case '*=': + test = function(value) { + return value && value.includes(attributeValue); + }; + break; + case '!=': + test = function(value) { + return attributeValue !== value; + }; + break; + default: + test = function(value) { + return !!value; + }; + } + + if (attributeValue === '' && /^[*$^]=$/.test(att.operator)) { + test = function() { + return false; + }; + } + + if (!test) { + test = function(value) { + return value && regexp.test(value); + }; + } + return { + key: attributeKey, + value: attributeValue, + test: test + }; + }); +} + +function convertClasses(classes) { + if (!classes) { + return; + } + return classes.map(className => { + className = className.replace(reUnescape, ''); + + return { + value: className, + regexp: new RegExp('(^|\\s)' + escapeRegExp(className) + '(\\s|$)') + }; + }); +} + +function convertPseudos(pseudos) { + if (!pseudos) { + return; + } + return pseudos.map(p => { + var expressions; + + if (p.name === 'not') { + expressions = p.value; + expressions = expressions.selectors + ? expressions.selectors + : [expressions]; + expressions = convertExpressions(expressions); + } + return { + name: p.name, + expressions: expressions, + value: p.value + }; + }); +} + +/** + * convert the css-selector-parser format into the Slick format + * @private + * @param Array {Object} expressions + * @return Array {Object} + * + */ +function convertExpressions(expressions) { + return expressions.map(exp => { + var newExp = []; + var rule = exp.rule; + while (rule) { + /* eslint no-restricted-syntax: 0 */ + // `.tagName` is a property coming from the `CSSSelectorParser` library + newExp.push({ + tag: rule.tagName ? rule.tagName.toLowerCase() : '*', + combinator: rule.nestingOperator ? rule.nestingOperator : ' ', + id: rule.id, + attributes: convertAttributes(rule.attrs), + classes: convertClasses(rule.classNames), + pseudos: convertPseudos(rule.pseudos) + }); + rule = rule.rule; + } + return newExp; + }); +} + +/** + * Convert a CSS selector to the Slick format expression + * + * @private + * @param {String} selector CSS selector to convert + * @returns {Object[]} Array of Slick format expressions + */ +axe.utils.convertSelector = function convertSelector(selector) { + var expressions = axe.utils.cssParser.parse(selector); + expressions = expressions.selectors ? expressions.selectors : [expressions]; + return convertExpressions(expressions); +}; + /** * Determine if a virtual node matches a Slick format CSS expression * diff --git a/test/core/utils/matches.js b/test/core/utils/matches.js index 51cdc9ed10..337ca0b4ce 100644 --- a/test/core/utils/matches.js +++ b/test/core/utils/matches.js @@ -1,4 +1,4 @@ -describe.only('utils.matches', function() { +describe('utils.matches', function() { var matches = axe.utils.matches; var fixture = document.querySelector('#fixture'); var queryFixture = axe.testUtils.queryFixture; From 77682dcff97e2b3d5734b023a4b3b31e62e7360a Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Mon, 20 Jan 2020 08:49:15 -0700 Subject: [PATCH 5/7] fixes --- lib/commons/matches/attributes.js | 21 +++++++++++---------- lib/commons/matches/from-definition.js | 21 +++++++++++---------- lib/commons/matches/index.js | 20 +++++++++++--------- lib/commons/matches/node-name.js | 19 +++++++++---------- lib/commons/matches/properties.js | 21 +++++++++++---------- 5 files changed, 53 insertions(+), 49 deletions(-) diff --git a/lib/commons/matches/attributes.js b/lib/commons/matches/attributes.js index 81b1472962..e5d8b7f699 100644 --- a/lib/commons/matches/attributes.js +++ b/lib/commons/matches/attributes.js @@ -1,26 +1,27 @@ /* global matches */ /** - * Check if a node matches some attribute(s) + * Check if a virtual node matches some attribute(s) * - * Note: matches.attributes(node, matcher) can be indirectly used through - * matches(node, { attributes: matcher }) + * Note: matches.attributes(vNode, matcher) can be indirectly used through + * matches(vNode, { attributes: matcher }) * * Example: * ```js - * matches.attributes(node, { + * matches.attributes(vNode, { * 'aria-live': 'assertive', // Simple string match * 'aria-expanded': /true|false/i, // either boolean, case insensitive * }) * ``` * - * @param {HTMLElement|VirtualNode} node + * @deprecated HTMLElement is deprecated, use VirtualNode instead + * + * @param {HTMLElement|VirtualNode} vNode * @param {Object} Attribute matcher * @returns {Boolean} */ -matches.attributes = function matchesAttributes(node, matcher) { - const vNode = - node instanceof axe.AbstractVirtualNode - ? node - : axe.utils.getNodeFromTree(node); +matches.attributes = function matchesAttributes(vNode, matcher) { + if (!(vNode instanceof axe.AbstractVirtualNode)) { + vNode = axe.utils.getNodeFromTree(vNode); + } return matches.fromFunction(attrName => vNode.attr(attrName), matcher); }; diff --git a/lib/commons/matches/from-definition.js b/lib/commons/matches/from-definition.js index 020d25bbb1..62b4f6e629 100644 --- a/lib/commons/matches/from-definition.js +++ b/lib/commons/matches/from-definition.js @@ -2,14 +2,14 @@ const matchers = ['nodeName', 'attributes', 'properties', 'condition']; /** - * Check if a node matches some definition + * Check if a virtual node matches some definition * - * Note: matches.fromDefinition(node, definition) can be indirectly used through - * matches(node, definition) + * Note: matches.fromDefinition(vNode, definition) can be indirectly used through + * matches(vNode, definition) * * Example: * ```js - * matches.fromDefinition(node, { + * matches.fromDefinition(vNode, { * nodeName: ['div', 'span'] * attributes: { * 'aria-live': 'assertive' @@ -17,16 +17,17 @@ const matchers = ['nodeName', 'attributes', 'properties', 'condition']; * }) * ``` * + * @deprecated HTMLElement is deprecated, use VirtualNode instead + * * @private - * @param {HTMLElement|VirtualNode} node + * @param {HTMLElement|VirtualNode} vNode * @param {Object|Array} definition * @returns {Boolean} */ -matches.fromDefinition = function matchFromDefinition(node, definition) { - const vNode = - node instanceof axe.AbstractVirtualNode - ? node - : axe.utils.getNodeFromTree(node); +matches.fromDefinition = function matchFromDefinition(vNode, definition) { + if (!(vNode instanceof axe.AbstractVirtualNode)) { + vNode = axe.utils.getNodeFromTree(vNode); + } if (Array.isArray(definition)) { return definition.some(definitionItem => matches(vNode, definitionItem)); diff --git a/lib/commons/matches/index.js b/lib/commons/matches/index.js index 917b050cfe..621866684b 100644 --- a/lib/commons/matches/index.js +++ b/lib/commons/matches/index.js @@ -1,37 +1,39 @@ /* exported matches */ /** - * Check if a DOM element matches a definition + * Check if a virtual node matches a definition * * Example: * ```js * // Match a single nodeName: - * axe.commons.matches(elm, 'div') + * axe.commons.matches(vNode, 'div') * * // Match one of multiple nodeNames: - * axe.commons.matches(elm, ['ul', 'ol']) + * axe.commons.matches(vNode, ['ul', 'ol']) * * // Match a node with nodeName 'button' and with aria-hidden: true: - * axe.commons.matches(elm, { + * axe.commons.matches(vNode, { * nodeName: 'button', * attributes: { 'aria-hidden': 'true' } * }) * * // Mixed input. Match button nodeName, input[type=button] and input[type=reset] - * axe.commons.matches(elm, ['button', { + * axe.commons.matches(vNode, ['button', { * nodeName: 'input', // nodeName match isn't case sensitive * properties: { type: ['button', 'reset'] } * }]) * ``` * + * @deprecated HTMLElement is deprecated, use VirtualNode instead + * * @namespace matches * @memberof axe.commons - * @param {HTMLElement|VirtualNode} node node to verify attributes against constraints + * @param {HTMLElement|VirtualNode} vNode virtual node to verify attributes against constraints * @param {Array|String|Object|Function|Regex} definition - * @return {Boolean} true/ false based on if node passes the constraints expected + * @return {Boolean} true/ false based on if vNode passes the constraints expected */ -function matches(node, definition) { - return matches.fromDefinition(node, definition); +function matches(vNode, definition) { + return matches.fromDefinition(vNode, definition); } commons.matches = matches; diff --git a/lib/commons/matches/node-name.js b/lib/commons/matches/node-name.js index 4c26083946..94fac0b58a 100644 --- a/lib/commons/matches/node-name.js +++ b/lib/commons/matches/node-name.js @@ -1,25 +1,24 @@ /* global matches */ /** - * Check if the nodeName of a node matches some value. + * Check if the nodeName of a virtual node matches some value. * - * Note: matches.nodeName(node, matcher) can be indirectly used through - * matches(node, { nodeName: matcher }) + * Note: matches.nodeName(vNode, matcher) can be indirectly used through + * matches(vNode, { nodeName: matcher }) * * Example: * ```js - * matches.nodeName(node, ['div', 'span']) + * matches.nodeName(vNode, ['div', 'span']) * ``` * * @deprecated HTMLElement is deprecated, use VirtualNode instead * - * @param {HTMLElement|VirtualNode} node + * @param {HTMLElement|VirtualNode} vNode * @param {Object} Attribute matcher * @returns {Boolean} */ -matches.nodeName = function matchNodeName(node, matcher) { - const vNode = - node instanceof axe.AbstractVirtualNode - ? node - : axe.utils.getNodeFromTree(node); +matches.nodeName = function matchNodeName(vNode, matcher) { + if (!(vNode instanceof axe.AbstractVirtualNode)) { + vNode = axe.utils.getNodeFromTree(vNode); + } return matches.fromPrimative(vNode.props.nodeName, matcher); }; diff --git a/lib/commons/matches/properties.js b/lib/commons/matches/properties.js index 940463aaae..41e1bec48b 100644 --- a/lib/commons/matches/properties.js +++ b/lib/commons/matches/properties.js @@ -1,27 +1,28 @@ /* global matches */ /** - * Check if a node matches some attribute(s) + * Check if a virtual node matches some attribute(s) * - * Note: matches.properties(node, matcher) can be indirectly used through - * matches(node, { properties: matcher }) + * Note: matches.properties(vNode, matcher) can be indirectly used through + * matches(vNode, { properties: matcher }) * * Example: * ```js - * matches.properties(node, { + * matches.properties(vNode, { * type: 'text', // Simple string match * value: value => value.trim() !== '', // None-empty value, using a function matcher * }) * ``` * - * @param {HTMLElement|VirtualNode} node + * @deprecated HTMLElement is deprecated, use VirtualNode instead + * + * @param {HTMLElement|VirtualNode} vNode * @param {Object} matcher * @returns {Boolean} */ -matches.properties = function matchesProperties(node, matcher) { - const vNode = - node instanceof axe.AbstractVirtualNode - ? node - : axe.utils.getNodeFromTree(node); +matches.properties = function matchesProperties(vNode, matcher) { + if (!(vNode instanceof axe.AbstractVirtualNode)) { + vNode = axe.utils.getNodeFromTree(vNode); + } return matches.fromFunction(propName => vNode.props[propName], matcher); }; From 72a5baa2cff0b074860c76a1c2797a6b7bd93a55 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 23 Jan 2020 13:54:59 -0700 Subject: [PATCH 6/7] test with serialNode --- test/commons/matches/attributes.js | 19 ++++++ test/commons/matches/from-definition.js | 77 +++++++++++++++++++++++++ test/commons/matches/node-name.js | 14 ++++- test/commons/matches/properties.js | 16 +++++ test/core/utils/matches.js | 7 +++ 5 files changed, 131 insertions(+), 2 deletions(-) diff --git a/test/commons/matches/attributes.js b/test/commons/matches/attributes.js index 6ee32b1459..476bb84b87 100644 --- a/test/commons/matches/attributes.js +++ b/test/commons/matches/attributes.js @@ -58,4 +58,23 @@ describe('matches.attributes', function() { }) ); }); + + it('works with SerialVirtualNode', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'span', + attributes: { + id: 'target', + foo: 'baz', + bar: 'foo', + baz: 'bar' + } + }); + assert.isTrue( + attributes(serialNode, { + foo: 'baz', + bar: 'foo', + baz: 'bar' + }) + ); + }); }); diff --git a/test/commons/matches/from-definition.js b/test/commons/matches/from-definition.js index 52b4f033c4..bd3c460647 100644 --- a/test/commons/matches/from-definition.js +++ b/test/commons/matches/from-definition.js @@ -183,6 +183,83 @@ describe('matches.fromDefinition', function() { }); }); + describe('with SerialVirtualNode', function() { + it('matches using a string', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + id: 'target' + } + }); + assert.isTrue(fromDefinition(serialNode, 'div')); + assert.isFalse(fromDefinition(serialNode, 'span')); + }); + + it('matches nodeName', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + id: 'target' + } + }); + assert.isTrue( + fromDefinition(serialNode, { + nodeName: 'div' + }) + ); + assert.isFalse( + fromDefinition(serialNode, { + nodeName: 'span' + }) + ); + }); + + it('matches attributes', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + id: 'target', + foo: 'bar' + } + }); + assert.isTrue( + fromDefinition(serialNode, { + attributes: { + foo: 'bar' + } + }) + ); + assert.isFalse( + fromDefinition(serialNode, { + attributes: { + foo: 'baz' + } + }) + ); + }); + + it('matches properties', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'input', + id: 'target' + }); + assert.isTrue( + fromDefinition(serialNode, { + properties: { + id: 'target' + } + }) + ); + assert.isFalse( + fromDefinition(serialNode, { + properties: { + id: 'bar' + } + }) + ); + }); + }); + describe('with a `condition` property', function() { it('calls condition and uses its return value as a matcher', function() { var virtualNode = queryFixture('
foo
'); diff --git a/test/commons/matches/node-name.js b/test/commons/matches/node-name.js index 0c3d29e34f..7a2ec2d53e 100644 --- a/test/commons/matches/node-name.js +++ b/test/commons/matches/node-name.js @@ -51,12 +51,22 @@ describe('matches.nodeName', function() { it('is case insensitive for HTML, but not for XHTML', function() { var virtualNode = queryFixture('

foo

'); - virtualNode._isXHTML = true; - assert.isFalse(matchNodeName(virtualNode, 'h1')); + virtualNode._isXHTML = false; + assert.isTrue(matchNodeName(virtualNode, 'h1')); }); it('works with actual nodes', function() { var virtualNode = queryFixture('

foo

'); assert.isTrue(matchNodeName(virtualNode.actualNode, 'h1')); }); + + it('works with SerialVirtualNode', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'h1', + attributes: { + id: 'target' + } + }); + assert.isTrue(matchNodeName(serialNode, 'h1')); + }); }); diff --git a/test/commons/matches/properties.js b/test/commons/matches/properties.js index 086ec15dd6..472e5ab409 100644 --- a/test/commons/matches/properties.js +++ b/test/commons/matches/properties.js @@ -54,4 +54,20 @@ describe('matches.properties', function() { }) ); }); + + it('works with SerialVirtualNode', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'input', + type: 'text', + id: 'target' + }); + + assert.isTrue( + properties(serialNode, { + nodeName: 'input', + id: 'target', + type: 'text' + }) + ); + }); }); diff --git a/test/core/utils/matches.js b/test/core/utils/matches.js index 337ca0b4ce..9dd2e71475 100644 --- a/test/core/utils/matches.js +++ b/test/core/utils/matches.js @@ -154,6 +154,13 @@ describe('utils.matches', function() { ); assert.isFalse(matches(virtualNode, 'span.foo[id="target"]:not(span)')); }); + + it('returns true if a comma-separated list of selectors match', function() { + var virtualNode = queryFixture( + '' + ); + assert.isFalse(matches(virtualNode, 'div, p, span')); + }); }); describe('combinator', function() { From ec7a3d8bf8495ab727f7633cb591b7903714594e Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 23 Jan 2020 14:00:06 -0700 Subject: [PATCH 7/7] match multiple expressions --- lib/core/utils/matches.js | 4 +++- test/core/utils/matches.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/core/utils/matches.js b/lib/core/utils/matches.js index a0a4e28cc0..96e960b715 100644 --- a/lib/core/utils/matches.js +++ b/lib/core/utils/matches.js @@ -257,5 +257,7 @@ axe.utils.matchesExpression = function matchesExpression( */ axe.utils.matches = function matches(vNode, selector) { let expressions = axe.utils.convertSelector(selector); - return axe.utils.matchesExpression(vNode, expressions[0]); + return expressions.some(expression => + axe.utils.matchesExpression(vNode, expression) + ); }; diff --git a/test/core/utils/matches.js b/test/core/utils/matches.js index 9dd2e71475..8479c98548 100644 --- a/test/core/utils/matches.js +++ b/test/core/utils/matches.js @@ -159,7 +159,7 @@ describe('utils.matches', function() { var virtualNode = queryFixture( '' ); - assert.isFalse(matches(virtualNode, 'div, p, span')); + assert.isTrue(matches(virtualNode, 'div, p, span')); }); });