From 289af761d707c14d5c2aa42d8fc44949c5ad1272 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Fri, 24 Mar 2017 15:28:10 -0400 Subject: [PATCH 001/142] add first implementation of getComposedTree with v1 tests --- lib/core/utils/composed-tree.js | 95 ++++++++++++++++++++++++++ test/core/utils/composed-tree.js | 110 +++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 lib/core/utils/composed-tree.js create mode 100644 test/core/utils/composed-tree.js diff --git a/lib/core/utils/composed-tree.js b/lib/core/utils/composed-tree.js new file mode 100644 index 0000000000..53991b4eb8 --- /dev/null +++ b/lib/core/utils/composed-tree.js @@ -0,0 +1,95 @@ +/* global console */ +var axe = axe || { utils: {} }; +/** + * NOTE: level only increases on "real" nodes because others do not exist in the composed tree + */ +axe.utils.printComposedTree = function (node, level) { + var indent = ' '.repeat(level) + '\u2514> '; + var nodeName; + if (node.shadowRoot) { + node.shadowRoot.childNodes.forEach(function (child) { + axe.utils.printComposedTree(child, level); + }); + } else { + nodeName = node.nodeName.toLowerCase(); + if (['style', 'template', 'script'].indexOf(nodeName) !== -1) { + return; + } + if (nodeName === 'content') { + node.getDistributedNodes().forEach(function (child) { + axe.utils.printComposedTree(child, level); + }); + } else if (nodeName === 'slot') { + node.assignedNodes().forEach(function (child) { + axe.utils.printComposedTree(child, level); + }); + } else { + if (node.nodeType === 1) { + console.log(indent, node); + node.childNodes.forEach(function (child) { + axe.utils.printComposedTree(child, level + 1); + }); + } + } + } +}; + +function virtualDOMfromNode (node, shadowId) { + // todo: attributes'n shit (maybe) + return { + shadowId: shadowId, + children: [], + actualNode: node + }; +} + +/** + * recursvely returns an array of the virtual DOM nodes at this level + * excluding comment nodes and of course the shadow DOM nodes + * and + * + * @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.getComposedTree = function (node, shadowId) { + // using a closure here and therefore cannot easily refactor toreduce the statements + //jshint maxstatements: false + var retVal, realArray, nodeName; + function reduceShadowDOM (res, child) { + var replacements = axe.utils.getComposedTree(child, shadowId); + if (replacements) { + res = res.concat(replacements); + } + return res; + } + + if (node.shadowRoot) { + // generate an ID for this shadow root and overwrite the current + // closure shadowId with this value so that it cascades down the tree + shadowId = 'a' + Math.random().toString().substring(2); + realArray = Array.from(node.shadowRoot.childNodes); + return realArray.reduce(reduceShadowDOM, []); + } else { + nodeName = node.nodeName.toLowerCase(); + if (nodeName === 'content') { + realArray = Array.from(node.getDistributedNodes()); + return realArray.reduce(reduceShadowDOM, []); + } else if (nodeName === 'slot') { + realArray = Array.from(node.assignedNodes()); + return realArray.reduce(reduceShadowDOM, []); + } else { + if (node.nodeType === 1) { + retVal = virtualDOMfromNode(node, shadowId); + realArray = Array.from(node.childNodes); + retVal.children = realArray.reduce(reduceShadowDOM, []); + return [retVal]; + } else if (node.nodeType === 3) { + // text + return [virtualDOMfromNode(node)]; + } + return undefined; + } + } +}; diff --git a/test/core/utils/composed-tree.js b/test/core/utils/composed-tree.js new file mode 100644 index 0000000000..9509402e28 --- /dev/null +++ b/test/core/utils/composed-tree.js @@ -0,0 +1,110 @@ +var fixture = document.getElementById('fixture'); + +if (document.body && typeof document.body.createShadowRoot === 'function') { + describe('composed-tree shadow DOM v0', function () { + 'use strict'; + afterEach(function () { + fixture.innerHTML = ''; + }); + it('shadow DOM v0'); + }); +} + +if (document.body && typeof document.body.attachShadow === 'function') { + describe('composed-tree shadow DOM v1', function () { + 'use strict'; + afterEach(function () { + fixture.innerHTML = ''; + }); + beforeEach(function () { + function createStoryGroup (className, slotName) { + var group = document.createElement('div'); + group.className = className; + // Empty string in slot name attribute or absence thereof work the same, so no need for special handling. + group.innerHTML = '
'; + return group; + } + + function createStyle () { + var style = document.createElement('style'); + style.textContent = 'div.breaking { color: Red;font-size: 20px; border: 1px dashed Purple; }' + + 'div.other { padding: 2px 0 0 0; border: 1px solid Cyan; }'; + return style; + } + + function makeShadowTree (storyList) { + var root = storyList.attachShadow({mode: 'open'}); + root.appendChild(createStyle()); + root.appendChild(createStoryGroup('breaking', 'breaking')); + root.appendChild(createStoryGroup('other', '')); + } + var str = '
  • 1
  • ' + + '
  • 2
  • 3
  • ' + + '
  • 4
  • 5
  • 6
  • '; + str += '
  • 1
  • ' + + '
  • 2
  • 3
  • ' + + '
  • 4
  • 5
  • 6
  • '; + fixture.innerHTML = str; + + fixture.querySelectorAll('.stories').forEach(makeShadowTree); + }); + it('should support shadow DOM v1', function () { + assert.isDefined(fixture.firstChild.shadowRoot); + }); + it('getComposedTree should return an array of stuff', function () { + assert.isTrue(Array.isArray(axe.utils.getComposedTree(fixture.firstChild))); + }); + it('getComposedTree\'s virtual DOM should represent the composed tree', function () { + var virtualDOM = axe.utils.getComposedTree(fixture.firstChild); + assert.equal(virtualDOM.length, 3); + assert.equal(virtualDOM[0].actualNode.nodeName, 'STYLE'); + + // breaking news stories + assert.equal(virtualDOM[1].actualNode.nodeName, 'DIV'); + assert.equal(virtualDOM[1].actualNode.className, 'breaking'); + + // other news stories + assert.equal(virtualDOM[2].actualNode.nodeName, 'DIV'); + assert.equal(virtualDOM[2].actualNode.className, 'other'); + + // breaking + assert.equal(virtualDOM[1].children.length, 1); + assert.equal(virtualDOM[1].children[0].actualNode.nodeName, 'UL'); + virtualDOM[1].children[0].children.forEach(function (child, index) { + assert.equal(child.actualNode.nodeName, 'LI'); + assert.isTrue(child.actualNode.textContent === 3*(index + 1) + ''); + }); + assert.equal(virtualDOM[1].children[0].children.length, 2); + + // other + assert.equal(virtualDOM[2].children.length, 1); + assert.equal(virtualDOM[2].children[0].actualNode.nodeName, 'UL'); + assert.equal(virtualDOM[2].children[0].children.length, 4); + }); + it('getComposedTree\'s virtual DOM should give an ID to the shadow DOM', function () { + var virtualDOM = axe.utils.getComposedTree(fixture); + assert.isUndefined(virtualDOM[0].shadowId); + assert.isDefined(virtualDOM[0].children[0].shadowId); + assert.isDefined(virtualDOM[0].children[1].shadowId); + assert.isDefined(virtualDOM[0].children[4].shadowId); + // shadow IDs in the same shadowRoot must be the same + assert.equal(virtualDOM[0].children[0].shadowId, + virtualDOM[0].children[1].shadowId); + // should cascade + assert.equal(virtualDOM[0].children[1].shadowId, + virtualDOM[0].children[1].children[0].shadowId); + // shadow IDs in different shadowRoots must be different + assert.notEqual(virtualDOM[0].children[0].shadowId, + virtualDOM[0].children[4].shadowId); + + }); + }); +} + +if (document.body && typeof document.body.attachShadow === 'undefined' && + typeof document.body.createShadowRoot === 'undefined') { + describe('composed-tree', function () { + 'use strict'; + it('SHADOW DOM TESTS DEFERRED, NO SUPPORT'); + }); +} From 2d8c40a8752a75b408e0629f9da5aa8986851966 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Fri, 24 Mar 2017 16:58:03 -0400 Subject: [PATCH 002/142] add tests for shadow DOM v0 --- test/core/utils/composed-tree.js | 145 ++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 52 deletions(-) diff --git a/test/core/utils/composed-tree.js b/test/core/utils/composed-tree.js index 9509402e28..5772daf92c 100644 --- a/test/core/utils/composed-tree.js +++ b/test/core/utils/composed-tree.js @@ -1,12 +1,102 @@ var fixture = document.getElementById('fixture'); +function createStyle () { + 'use strict'; + + var style = document.createElement('style'); + style.textContent = 'div.breaking { color: Red;font-size: 20px; border: 1px dashed Purple; }' + + 'div.other { padding: 2px 0 0 0; border: 1px solid Cyan; }'; + return style; +} + +function composedTreeAssertions () { + 'use strict'; + + var virtualDOM = axe.utils.getComposedTree(fixture.firstChild); + assert.equal(virtualDOM.length, 3); + assert.equal(virtualDOM[0].actualNode.nodeName, 'STYLE'); + + // breaking news stories + assert.equal(virtualDOM[1].actualNode.nodeName, 'DIV'); + assert.equal(virtualDOM[1].actualNode.className, 'breaking'); + + // other news stories + assert.equal(virtualDOM[2].actualNode.nodeName, 'DIV'); + assert.equal(virtualDOM[2].actualNode.className, 'other'); + + // breaking + assert.equal(virtualDOM[1].children.length, 1); + assert.equal(virtualDOM[1].children[0].actualNode.nodeName, 'UL'); + virtualDOM[1].children[0].children.forEach(function (child, index) { + assert.equal(child.actualNode.nodeName, 'LI'); + assert.isTrue(child.actualNode.textContent === 3*(index + 1) + ''); + }); + assert.equal(virtualDOM[1].children[0].children.length, 2); + + // other + assert.equal(virtualDOM[2].children.length, 1); + assert.equal(virtualDOM[2].children[0].actualNode.nodeName, 'UL'); + assert.equal(virtualDOM[2].children[0].children.length, 4); +} + +function shadowIdAssertions () { + 'use strict'; + + var virtualDOM = axe.utils.getComposedTree(fixture); + assert.isUndefined(virtualDOM[0].shadowId); + assert.isDefined(virtualDOM[0].children[0].shadowId); + assert.isDefined(virtualDOM[0].children[1].shadowId); + assert.isDefined(virtualDOM[0].children[4].shadowId); + // shadow IDs in the same shadowRoot must be the same + assert.equal(virtualDOM[0].children[0].shadowId, + virtualDOM[0].children[1].shadowId); + // should cascade + assert.equal(virtualDOM[0].children[1].shadowId, + virtualDOM[0].children[1].children[0].shadowId); + // shadow IDs in different shadowRoots must be different + assert.notEqual(virtualDOM[0].children[0].shadowId, + virtualDOM[0].children[4].shadowId); + +} + if (document.body && typeof document.body.createShadowRoot === 'function') { describe('composed-tree shadow DOM v0', function () { 'use strict'; afterEach(function () { fixture.innerHTML = ''; }); - it('shadow DOM v0'); + beforeEach(function () { + function createStoryGroup (className, contentSelector) { + var group = document.createElement('div'); + group.className = className; + group.innerHTML = '
    '; + return group; + } + + function makeShadowTree (storyList) { + var root = storyList.createShadowRoot(); + root.appendChild(createStyle()); + root.appendChild(createStoryGroup('breaking', '.breaking')); + root.appendChild(createStoryGroup('other', '')); + } + var str = '
  • 1
  • ' + + '
  • 2
  • 3
  • ' + + '
  • 4
  • 5
  • 6
  • '; + str += '
  • 1
  • ' + + '
  • 2
  • 3
  • ' + + '
  • 4
  • 5
  • 6
  • '; + fixture.innerHTML = str; + + fixture.querySelectorAll('.stories').forEach(makeShadowTree); + }); + it('it should support shadow DOM v0', function () { + assert.isDefined(fixture.firstChild.shadowRoot); + }); + it('getComposedTree should return an array of stuff', function () { + assert.isTrue(Array.isArray(axe.utils.getComposedTree(fixture.firstChild))); + }); + it('getComposedTree\'s virtual DOM should represent the composed tree', composedTreeAssertions); + it('getComposedTree\'s virtual DOM should give an ID to the shadow DOM', shadowIdAssertions); }); } @@ -25,13 +115,6 @@ if (document.body && typeof document.body.attachShadow === 'function') { return group; } - function createStyle () { - var style = document.createElement('style'); - style.textContent = 'div.breaking { color: Red;font-size: 20px; border: 1px dashed Purple; }' + - 'div.other { padding: 2px 0 0 0; border: 1px solid Cyan; }'; - return style; - } - function makeShadowTree (storyList) { var root = storyList.attachShadow({mode: 'open'}); root.appendChild(createStyle()); @@ -54,50 +137,8 @@ if (document.body && typeof document.body.attachShadow === 'function') { it('getComposedTree should return an array of stuff', function () { assert.isTrue(Array.isArray(axe.utils.getComposedTree(fixture.firstChild))); }); - it('getComposedTree\'s virtual DOM should represent the composed tree', function () { - var virtualDOM = axe.utils.getComposedTree(fixture.firstChild); - assert.equal(virtualDOM.length, 3); - assert.equal(virtualDOM[0].actualNode.nodeName, 'STYLE'); - - // breaking news stories - assert.equal(virtualDOM[1].actualNode.nodeName, 'DIV'); - assert.equal(virtualDOM[1].actualNode.className, 'breaking'); - - // other news stories - assert.equal(virtualDOM[2].actualNode.nodeName, 'DIV'); - assert.equal(virtualDOM[2].actualNode.className, 'other'); - - // breaking - assert.equal(virtualDOM[1].children.length, 1); - assert.equal(virtualDOM[1].children[0].actualNode.nodeName, 'UL'); - virtualDOM[1].children[0].children.forEach(function (child, index) { - assert.equal(child.actualNode.nodeName, 'LI'); - assert.isTrue(child.actualNode.textContent === 3*(index + 1) + ''); - }); - assert.equal(virtualDOM[1].children[0].children.length, 2); - - // other - assert.equal(virtualDOM[2].children.length, 1); - assert.equal(virtualDOM[2].children[0].actualNode.nodeName, 'UL'); - assert.equal(virtualDOM[2].children[0].children.length, 4); - }); - it('getComposedTree\'s virtual DOM should give an ID to the shadow DOM', function () { - var virtualDOM = axe.utils.getComposedTree(fixture); - assert.isUndefined(virtualDOM[0].shadowId); - assert.isDefined(virtualDOM[0].children[0].shadowId); - assert.isDefined(virtualDOM[0].children[1].shadowId); - assert.isDefined(virtualDOM[0].children[4].shadowId); - // shadow IDs in the same shadowRoot must be the same - assert.equal(virtualDOM[0].children[0].shadowId, - virtualDOM[0].children[1].shadowId); - // should cascade - assert.equal(virtualDOM[0].children[1].shadowId, - virtualDOM[0].children[1].children[0].shadowId); - // shadow IDs in different shadowRoots must be different - assert.notEqual(virtualDOM[0].children[0].shadowId, - virtualDOM[0].children[4].shadowId); - - }); + it('getComposedTree\'s virtual DOM should represent the composed tree', composedTreeAssertions); + it('getComposedTree\'s virtual DOM should give an ID to the shadow DOM', shadowIdAssertions); }); } From a2b37e9ecf8f491bee6285fca5eeded532fb3d9c Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sat, 25 Mar 2017 14:30:04 -0400 Subject: [PATCH 003/142] Add the Slick CSS selector parser to the utils --- lib/core/utils/slick-parser.js | 218 +++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 lib/core/utils/slick-parser.js diff --git a/lib/core/utils/slick-parser.js b/lib/core/utils/slick-parser.js new file mode 100644 index 0000000000..613a012b3a --- /dev/null +++ b/lib/core/utils/slick-parser.js @@ -0,0 +1,218 @@ +/* jshint ignore:start */ +/* +--- +name: Slick.Parser +description: Standalone CSS3 Selector parser +provides: Slick.Parser +... +*/ +(function (axe) { + var parsed, + separatorIndex, + combinatorIndex, + reversed, + cache = {}, + reverseCache = {}, + reUnescape = /\\/g; + + var parse = function(expression, isReversed){ + if (expression == null) return null; + if (expression.Slick === true) return expression; + expression = ('' + expression).replace(/^\s+|\s+$/g, ''); + reversed = !!isReversed; + var currentCache = (reversed) ? reverseCache : cache; + if (currentCache[expression]) return currentCache[expression]; + parsed = { + Slick: true, + expressions: [], + raw: expression, + reverse: function(){ + return parse(this.raw, true); + } + }; + separatorIndex = -1; + while (expression != (expression = expression.replace(regexp, parser))); + parsed.length = parsed.expressions.length; + return currentCache[parsed.raw] = (reversed) ? reverse(parsed) : parsed; + }; + + var reverseCombinator = function(combinator){ + if (combinator === '!') return ' '; + else if (combinator === ' ') return '!'; + else if ((/^!/).test(combinator)) return combinator.replace(/^!/, ''); + else return '!' + combinator; + }; + + var reverse = function(expression){ + var expressions = expression.expressions; + for (var i = 0; i < expressions.length; i++){ + var exp = expressions[i]; + var last = {parts: [], tag: '*', combinator: reverseCombinator(exp[0].combinator)}; + + for (var j = 0; j < exp.length; j++){ + var cexp = exp[j]; + if (!cexp.reverseCombinator) cexp.reverseCombinator = ' '; + cexp.combinator = cexp.reverseCombinator; + delete cexp.reverseCombinator; + } + + exp.reverse().push(last); + } + return expression; + }; + + var escapeRegExp = (function(){ + // Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan MIT License + var from = /(?=[\-\[\]{}()*+?.\\\^$|,#\s])/g, to = '\\'; + return function(string){ return string.replace(from, to) } + }()) + + var regexp = new RegExp( + /* + #!/usr/bin/env ruby + puts "\t\t" + DATA.read.gsub(/\(\?x\)|\s+#.*$|\s+|\\$|\\n/,'') + __END__ + "(?x)^(?:\ + \\s* ( , ) \\s* # Separator \n\ + | \\s* ( + ) \\s* # Combinator \n\ + | ( \\s+ ) # CombinatorChildren \n\ + | ( + | \\* ) # Tag \n\ + | \\# ( + ) # ID \n\ + | \\. ( + ) # ClassName \n\ + | # Attribute \n\ + \\[ \ + \\s* (+) (?: \ + \\s* ([*^$!~|]?=) (?: \ + \\s* (?:\ + ([\"']?)(.*?)\\9 \ + )\ + ) \ + )? \\s* \ + \\](?!\\]) \n\ + | :+ ( + )(?:\ + \\( (?:\ + (?:([\"'])([^\\12]*)\\12)|((?:\\([^)]+\\)|[^()]*)+)\ + ) \\)\ + )?\ + )" + */ + "^(?:\\s*(,)\\s*|\\s*(+)\\s*|(\\s+)|(+|\\*)|\\#(+)|\\.(+)|\\[\\s*(+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)" + .replace(//, '[' + escapeRegExp(">+~`!@$%^&={}\\;/g, '(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])') + .replace(//g, '(?:[:\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])') + ); + + function parser( + rawMatch, + + separator, + combinator, + combinatorChildren, + + tagName, + id, + className, + + attributeKey, + attributeOperator, + attributeQuote, + attributeValue, + + pseudoMarker, + pseudoClass, + pseudoQuote, + pseudoClassQuotedValue, + pseudoClassValue + ){ + if (separator || separatorIndex === -1){ + parsed.expressions[++separatorIndex] = []; + combinatorIndex = -1; + if (separator) return ''; + } + + if (combinator || combinatorChildren || combinatorIndex === -1){ + combinator = combinator || ' '; + var currentSeparator = parsed.expressions[separatorIndex]; + if (reversed && currentSeparator[combinatorIndex]) + currentSeparator[combinatorIndex].reverseCombinator = reverseCombinator(combinator); + currentSeparator[++combinatorIndex] = {combinator: combinator, tag: '*'}; + } + + var currentParsed = parsed.expressions[separatorIndex][combinatorIndex]; + + if (tagName){ + currentParsed.tag = tagName.replace(reUnescape, ''); + + } else if (id){ + currentParsed.id = id.replace(reUnescape, ''); + + } else if (className){ + className = className.replace(reUnescape, ''); + + if (!currentParsed.classList) currentParsed.classList = []; + if (!currentParsed.classes) currentParsed.classes = []; + currentParsed.classList.push(className); + currentParsed.classes.push({ + value: className, + regexp: new RegExp('(^|\\s)' + escapeRegExp(className) + '(\\s|$)') + }); + + } else if (pseudoClass){ + pseudoClassValue = pseudoClassValue || pseudoClassQuotedValue; + pseudoClassValue = pseudoClassValue ? pseudoClassValue.replace(reUnescape, '') : null; + + if (!currentParsed.pseudos) currentParsed.pseudos = []; + currentParsed.pseudos.push({ + key: pseudoClass.replace(reUnescape, ''), + value: pseudoClassValue, + type: pseudoMarker.length == 1 ? 'class' : 'element' + }); + + } else if (attributeKey){ + attributeKey = attributeKey.replace(reUnescape, ''); + attributeValue = (attributeValue || '').replace(reUnescape, ''); + + var test, regexp; + + switch (attributeOperator){ + 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.indexOf(attributeValue) > -1; + }; break; + case '!=' : test = function(value){ + return attributeValue != value; + }; break; + default : test = function(value){ + return !!value; + }; + } + + if (attributeValue == '' && (/^[*$^]=$/).test(attributeOperator)) test = function(){ + return false; + }; + + if (!test) test = function(value){ + return value && regexp.test(value); + }; + + if (!currentParsed.attributes) currentParsed.attributes = []; + currentParsed.attributes.push({ + key: attributeKey, + operator: attributeOperator, + value: attributeValue, + test: test + }); + + } + + return ''; + }; + + axe.utils.cssParser = parse; +})(axe); From 1df25c11bcaf35a6ef3a59695bc0a289ae3655f6 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sat, 25 Mar 2017 14:34:26 -0400 Subject: [PATCH 004/142] add acknowledgement for the Slick parser --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ba14767284..b2264214e7 100644 --- a/README.md +++ b/README.md @@ -99,3 +99,7 @@ Read the [documentation on contributing](CONTRIBUTING.md) ## Projects using axe-core [List of projects using axe-core](doc/projects.md) + +## Acknowledgements + +Thanks to the [Slick Parser](https://github.com/mootools/slick/blob/master/Source/Slick.Parser.js) implementers for their contribution, we have used it in the shadowDOM support code. From be9b9a0e3894fe4d8f99d8529e546b1cfdfbcea3 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sat, 25 Mar 2017 14:47:37 -0400 Subject: [PATCH 005/142] add Slick parser copyright --- lib/core/utils/slick-parser.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/core/utils/slick-parser.js b/lib/core/utils/slick-parser.js index 613a012b3a..188ad8fb18 100644 --- a/lib/core/utils/slick-parser.js +++ b/lib/core/utils/slick-parser.js @@ -1,4 +1,5 @@ /* jshint ignore:start */ +/* Copyright Mootools Developers, licensed under the MIT license https://opensource.org/licenses/MIT */ /* --- name: Slick.Parser From 096c57187a4d61875b8ee23d8fd84d29e1926fcf Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sun, 26 Mar 2017 13:15:05 -0400 Subject: [PATCH 006/142] implement virtual DOM querySelectorAll --- lib/core/utils/qsa.js | 69 ++++++++++++++++++++ test/core/utils/qsa.js | 140 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 lib/core/utils/qsa.js create mode 100644 test/core/utils/qsa.js diff --git a/lib/core/utils/qsa.js b/lib/core/utils/qsa.js new file mode 100644 index 0000000000..5b679707f7 --- /dev/null +++ b/lib/core/utils/qsa.js @@ -0,0 +1,69 @@ +/** + * querySelectorAll implementation that works on the virtual DOM and + * supports shadowDOM + */ + +// todo: implement an option to follow aria-owns + +function matchesTag (node, exp) { + return exp.tag === '*' || node.nodeName.toLowerCase() === exp.tag; +} + +function matchesClasses (node, exp) { + return !exp.classes || exp.classes.reduce((result, cl) => { + return result && (node.className && node.className.match(cl.regexp)); + }, true); +} + +function matchesAttributes (node, exp) { + return !exp.attributes || exp.attributes.reduce((result, att) => { + var nodeAtt = node.getAttribute(att.key); + return result && nodeAtt && att.test(nodeAtt); + }, true); +} + +function matchesId (node, exp) { + return !exp.id || node.id === exp.id; +} + +function matchSelector (targets, exp, recurse) { + var result = []; + + if (exp.pseudos) { + throw new Error('matchSelector does not support pseudo selector: ' + exp.pseudos[0].key); + } + targets = Array.isArray(targets) ? targets : [targets]; + targets.forEach((target) => { + if (matchesTag(target.actualNode, exp) && + matchesClasses(target.actualNode, exp) && + matchesAttributes(target.actualNode, exp) && + matchesId(target.actualNode, exp)) { + result.push(target); + } + if (recurse) { + result = result.concat(matchSelector(target.children.filter((child) => { + return !exp.id || child.shadowId === target.shadowId; + }), exp, recurse)); + } + }); + return result; +} + +axe.utils.querySelectorAll = function (domTree, selector) { + domTree = Array.isArray(domTree) ? domTree : [domTree]; + + return axe.utils.cssParser(selector).expressions.reduce((collected, exprArr) => { + var candidates = domTree; + exprArr.forEach((exp, index) => { + var recurse = exp.combinator === '>' ? false : true; + if ([' ', '>'].indexOf(exp.combinator) === -1) { + throw new Error('axe.utils.querySelectorAll does not support the combinator: ' + exp.combinator); + } + exp.tag = exp.tag.toLowerCase(); // do this once + candidates = candidates.reduce((result, node) => { + return result.concat(matchSelector(index ? node.children : node, exp, recurse)); + }, []); + }); + return collected.concat(candidates); + }, []); +}; \ No newline at end of file diff --git a/test/core/utils/qsa.js b/test/core/utils/qsa.js new file mode 100644 index 0000000000..3a7f4a1760 --- /dev/null +++ b/test/core/utils/qsa.js @@ -0,0 +1,140 @@ +function Vnode (nodeName, className, attributes, id) { + 'use strict'; + this.nodeName = nodeName.toUpperCase(); + this.id = id; + this.attributes = attributes || []; + this.className = className; +} + +Vnode.prototype.getAttribute = function (att) { + 'use strict'; + var attribute = this.attributes.find(function (item) { + return item.key === att; + }); + return attribute ? attribute.value : ''; +}; + +describe('axe.utils.querySelectorAll', function () { + 'use strict'; + var dom; + afterEach(function () { + }); + beforeEach(function () { + dom = [{ + actualNode: new Vnode('html'), + children: [{ + actualNode: new Vnode('body'), + children: [{ + actualNode: new Vnode('div', 'first',[{ + key: 'data-a11yhero', + value: 'faulkner' + }]), + shadowId: 'a', + children: [{ + actualNode: new Vnode('ul'), + shadowId: 'a', + children: [{ + actualNode: new Vnode('li', 'breaking'), + shadowId: 'a', + children: [] + },{ + actualNode: new Vnode('li', 'breaking'), + shadowId: 'a', + children: [] + }] + }] + }, { + actualNode: new Vnode('div', '', [], 'one'), + children: [] + }, { + actualNode: new Vnode('div', 'second third'), + shadowId: 'b', + children: [{ + actualNode: new Vnode('ul'), + shadowId: 'b', + children: [{ + actualNode: new Vnode('li', undefined, [{ + key: 'role', + value: 'tab' + }], 'one'), + shadowId: 'b', + children: [] + },{ + actualNode: new Vnode('li', undefined, [{ + key: 'role', + value: 'button' + }], 'one'), + shadowId: 'c', + children: [] + }] + }] + }] + }] + }]; + }); + it('should find nodes using just the tag', function () { + var result = axe.utils.querySelectorAll(dom, 'li'); + assert.equal(result.length, 4); + }); + it('should find nodes using parent selector', function () { + var result = axe.utils.querySelectorAll(dom, 'ul > li'); + assert.equal(result.length, 4); + }); + it('should NOT find nodes using parent selector', function () { + var result = axe.utils.querySelectorAll(dom, 'div > li'); + assert.equal(result.length, 0); + }); + it('should find nodes using hierarchical selector', function () { + var result = axe.utils.querySelectorAll(dom, 'div li'); + assert.equal(result.length, 4); + }); + it('should find nodes using class selector', function () { + var result = axe.utils.querySelectorAll(dom, '.breaking'); + assert.equal(result.length, 2); + }); + it('should find nodes using hierarchical class selector', function () { + var result = axe.utils.querySelectorAll(dom, '.first .breaking'); + assert.equal(result.length, 2); + }); + it('should NOT find nodes using hierarchical class selector', function () { + var result = axe.utils.querySelectorAll(dom, '.second .breaking'); + assert.equal(result.length, 0); + }); + it('should find nodes using multiple class selector', function () { + var result = axe.utils.querySelectorAll(dom, '.second.third'); + assert.equal(result.length, 1); + }); + it('should find nodes using id', function () { + var result = axe.utils.querySelectorAll(dom, '#one'); + assert.equal(result.length, 1); + }); + it('should find nodes using id, within a shadow DOM', function () { + var result = axe.utils.querySelectorAll(dom[0].children[0].children[2], '#one'); + assert.equal(result.length, 1); + }); + it('should find nodes using attribute', function () { + var result = axe.utils.querySelectorAll(dom, '[role]'); + assert.equal(result.length, 2); + }); + it('should find nodes using attribute with value', function () { + var result = axe.utils.querySelectorAll(dom, '[role=tab]'); + assert.equal(result.length, 1); + }); + it('should find nodes using attribute with value', function () { + var result = axe.utils.querySelectorAll(dom, '[role="button"]'); + assert.equal(result.length, 1); + }); + it('should find nodes using parent attribute with value', function () { + var result = axe.utils.querySelectorAll(dom, '[data-a11yhero="faulkner"] > ul'); + assert.equal(result.length, 1); + }); + it('should find nodes using hierarchical attribute with value', function () { + var result = axe.utils.querySelectorAll(dom, '[data-a11yhero="faulkner"] li'); + assert.equal(result.length, 2); + }); + it('should put it all together', function () { + var result = axe.utils.querySelectorAll(dom, + '.first[data-a11yhero="faulkner"] > ul li.breaking'); + assert.equal(result.length, 2); + }); +}); From dcd4e1a83877f96f1c8dd9e7270a06175886c248 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sun, 26 Mar 2017 19:03:47 -0400 Subject: [PATCH 007/142] make use of axe.utils.querySelectorAll and get all tests to work --- lib/checks/navigation/href-no-hash.js | 2 +- lib/checks/navigation/href-no-hash.json | 4 +- lib/core/base/check.js | 2 +- lib/core/base/context.js | 23 +++++--- lib/core/base/rule.js | 6 +- lib/core/utils/composed-tree.js | 8 ++- lib/core/utils/contains.js | 6 +- lib/core/utils/get-selector.js | 1 + lib/core/utils/node-sorter.js | 4 +- lib/core/utils/qsa.js | 4 +- lib/core/utils/select.js | 14 +++-- lib/rules/definition-list.json | 3 +- lib/rules/dlitem.json | 3 +- lib/rules/frame-title-unique.json | 2 +- lib/rules/label-matches.js | 14 +++++ lib/rules/label-title-only.json | 3 +- lib/rules/label.json | 3 +- lib/rules/link-in-text-block-matches.js | 4 ++ lib/rules/link-in-text-block.json | 2 +- lib/rules/link-name.json | 3 +- lib/rules/list.json | 3 +- lib/rules/listitem.json | 3 +- lib/rules/no-role-matches.js | 1 + lib/rules/not-html-matches.js | 1 + lib/rules/role-not-button-matches.js | 1 + lib/rules/valid-lang.json | 3 +- test/core/base/audit.js | 8 +-- test/core/base/check.js | 2 +- test/core/base/context.js | 76 +++++++++++++------------ test/core/base/rule.js | 55 +++++++++--------- test/core/public/run-rules.js | 2 +- test/core/utils/contains.js | 40 +++++++------ test/core/utils/node-sorter.js | 10 ++-- test/core/utils/qsa.js | 3 +- test/core/utils/select.js | 41 +++++++------ 35 files changed, 213 insertions(+), 147 deletions(-) create mode 100644 lib/rules/label-matches.js create mode 100644 lib/rules/no-role-matches.js create mode 100644 lib/rules/not-html-matches.js create mode 100644 lib/rules/role-not-button-matches.js diff --git a/lib/checks/navigation/href-no-hash.js b/lib/checks/navigation/href-no-hash.js index fff3a1699a..d89cc23034 100644 --- a/lib/checks/navigation/href-no-hash.js +++ b/lib/checks/navigation/href-no-hash.js @@ -1,6 +1,6 @@ var href = node.getAttribute('href'); -if(href === '#'){ +if (href === '#') { return false; } diff --git a/lib/checks/navigation/href-no-hash.json b/lib/checks/navigation/href-no-hash.json index 2652999aaa..45a3d770fd 100644 --- a/lib/checks/navigation/href-no-hash.json +++ b/lib/checks/navigation/href-no-hash.json @@ -4,8 +4,8 @@ "metadata": { "impact": "moderate", "messages": { - "pass": "Anchor does not have a href quals #", - "fail": "Anchor has a href quals #" + "pass": "Anchor does not have an href that equals #", + "fail": "Anchor has an href that equals #" } } } diff --git a/lib/core/base/check.js b/lib/core/base/check.js index 9e94adcdf0..b8034641dd 100644 --- a/lib/core/base/check.js +++ b/lib/core/base/check.js @@ -70,7 +70,7 @@ Check.prototype.run = function (node, options, resolve, reject) { var result; try { - result = this.evaluate.call(checkHelper, node, checkOptions); + result = this.evaluate.call(checkHelper, node.actualNode, checkOptions, node); } catch (e) { reject(e); return; diff --git a/lib/core/base/context.js b/lib/core/base/context.js index 327fc69f8d..cbb0c8aa1f 100644 --- a/lib/core/base/context.js +++ b/lib/core/base/context.js @@ -121,22 +121,31 @@ function parseSelectorArray(context, type) { 'use strict'; var item, - result = []; + result = [], nodeList; for (var i = 0, l = context[type].length; i < l; i++) { item = context[type][i]; // selector if (typeof item === 'string') { - result = result.concat(axe.utils.toArray(document.querySelectorAll(item))); + nodeList = Array.from(document.querySelectorAll(item)); + //jshint loopfunc:true + result = result.concat(nodeList.map((node) => { + return axe.utils.getComposedTree(node)[0]; + })); break; } else if (item && item.length && !(item instanceof Node)) { if (item.length > 1) { pushUniqueFrameSelector(context, type, item); } else { - result = result.concat(axe.utils.toArray(document.querySelectorAll(item[0]))); + nodeList = Array.from(document.querySelectorAll(item[0])); + //jshint loopfunc:true + result = result.concat(nodeList.map((node) => { + return axe.utils.getComposedTree(node)[0]; + })); } - } else { - result.push(item); + } else if (item instanceof Node) { + + result.push(axe.utils.getComposedTree(item)[0]); } } @@ -207,11 +216,11 @@ function Context(spec) { axe.utils.select('frame, iframe', this).forEach(function (frame) { if (isNodeInContext(frame, self)) { - pushUniqueFrame(self.frames, frame); + pushUniqueFrame(self.frames, frame.actualNode); } }); - if (this.include.length === 1 && this.include[0] === document) { + if (this.include.length === 1 && this.include[0].actualNode === document.documentElement) { this.page = true; } diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js index 4c6aaf7a3c..9a26659381 100644 --- a/lib/core/base/rule.js +++ b/lib/core/base/rule.js @@ -92,7 +92,7 @@ Rule.prototype.gather = function (context) { var elements = axe.utils.select(this.selector, context); if (this.excludeHidden) { return elements.filter(function (element) { - return !axe.utils.isHidden(element); + return !axe.utils.isHidden(element.actualNode); }); } return elements; @@ -134,7 +134,7 @@ Rule.prototype.run = function (context, options, resolve, reject) { try { // Matches throws an error when it lacks support for document methods nodes = this.gather(context) - .filter(node => this.matches(node)); + .filter(node => this.matches(node.actualNode)); } catch (error) { // Exit the rule execution if matches fails reject(new SupportError({cause: error, ruleId: this.id})); @@ -171,7 +171,7 @@ Rule.prototype.run = function (context, options, resolve, reject) { } }); if (hasResults) { - result.node = new axe.utils.DqElement(node); + result.node = new axe.utils.DqElement(node.actualNode); ruleResult.nodes.push(result); } } diff --git a/lib/core/utils/composed-tree.js b/lib/core/utils/composed-tree.js index 53991b4eb8..954f62227a 100644 --- a/lib/core/utils/composed-tree.js +++ b/lib/core/utils/composed-tree.js @@ -65,14 +65,18 @@ axe.utils.getComposedTree = function (node, shadowId) { return res; } - if (node.shadowRoot) { + if (node.documentElement) { // document + node = node.documentElement; + } + nodeName = node.nodeName.toLowerCase(); + // for some reason Chrome's marquee element has an open shadow DOM + if (node.shadowRoot && nodeName !== 'marquee') { // generate an ID for this shadow root and overwrite the current // closure shadowId with this value so that it cascades down the tree shadowId = 'a' + Math.random().toString().substring(2); realArray = Array.from(node.shadowRoot.childNodes); return realArray.reduce(reduceShadowDOM, []); } else { - nodeName = node.nodeName.toLowerCase(); if (nodeName === 'content') { realArray = Array.from(node.getDistributedNodes()); return realArray.reduce(reduceShadowDOM, []); diff --git a/lib/core/utils/contains.js b/lib/core/utils/contains.js index 2ba198d5e2..04b68bc67c 100644 --- a/lib/core/utils/contains.js +++ b/lib/core/utils/contains.js @@ -9,10 +9,10 @@ axe.utils.contains = function (node, otherNode) { //jshint bitwise: false 'use strict'; - if (typeof node.contains === 'function') { - return node.contains(otherNode); + if (typeof node.actualNode.contains === 'function') { + return node.actualNode.contains(otherNode.actualNode); } - return !!(node.compareDocumentPosition(otherNode) & 16); + return !!(node.actualNode.compareDocumentPosition(otherNode.actualNode) & 16); }; \ No newline at end of file diff --git a/lib/core/utils/get-selector.js b/lib/core/utils/get-selector.js index 7d339b6ff1..1179251c3e 100644 --- a/lib/core/utils/get-selector.js +++ b/lib/core/utils/get-selector.js @@ -58,6 +58,7 @@ function siblingsHaveSameSelector(node, selector) { * @return {String} Unique CSS selector for the node */ axe.utils.getSelector = function getSelector(node) { + //todo: implement shadowDOM support //jshint maxstatements: 21 'use strict'; diff --git a/lib/core/utils/node-sorter.js b/lib/core/utils/node-sorter.js index b92829aa81..d4aa8972fe 100644 --- a/lib/core/utils/node-sorter.js +++ b/lib/core/utils/node-sorter.js @@ -10,11 +10,11 @@ axe.utils.nodeSorter = function nodeSorter(a, b) { 'use strict'; - if (a === b) { + if (a.actualNode === b.actualNode) { return 0; } - if (a.compareDocumentPosition(b) & 4) { // a before b + if (a.actualNode.compareDocumentPosition(b.actualNode) & 4) { // a before b return -1; } diff --git a/lib/core/utils/qsa.js b/lib/core/utils/qsa.js index 5b679707f7..3690e78216 100644 --- a/lib/core/utils/qsa.js +++ b/lib/core/utils/qsa.js @@ -6,7 +6,7 @@ // todo: implement an option to follow aria-owns function matchesTag (node, exp) { - return exp.tag === '*' || node.nodeName.toLowerCase() === exp.tag; + return node.nodeType === 1 && (exp.tag === '*' || node.nodeName.toLowerCase() === exp.tag); } function matchesClasses (node, exp) { @@ -18,7 +18,7 @@ function matchesClasses (node, exp) { function matchesAttributes (node, exp) { return !exp.attributes || exp.attributes.reduce((result, att) => { var nodeAtt = node.getAttribute(att.key); - return result && nodeAtt && att.test(nodeAtt); + return result && nodeAtt !== null && (!att.value || att.test(nodeAtt)); }, true); } diff --git a/lib/core/utils/select.js b/lib/core/utils/select.js index 54156d4a63..43389ca208 100644 --- a/lib/core/utils/select.js +++ b/lib/core/utils/select.js @@ -49,17 +49,20 @@ function pushNode(result, nodes, context) { 'use strict'; for (var i = 0, l = nodes.length; i < l; i++) { - if (result.indexOf(nodes[i]) === -1 && isNodeInContext(nodes[i], context)) { + //jshint loopfunc:true + if (!result.find(function (item) { + return item.actualNode === nodes[i].actualNode; + }) && isNodeInContext(nodes[i], context)) { result.push(nodes[i]); } } } /** - * Selects elements which match `select` that are included and excluded via the `Context` object + * Selects elements which match `selector` that are included and excluded via the `Context` object * @param {String} selector CSS selector of the HTMLElements to select * @param {Context} context The "resolved" context object, @see Context - * @return {Array} Matching nodes sorted by DOM order + * @return {Array} Matching virtual DOM nodes sorted by DOM order */ axe.utils.select = function select(selector, context) { 'use strict'; @@ -67,10 +70,11 @@ axe.utils.select = function select(selector, context) { var result = [], candidate; for (var i = 0, l = context.include.length; i < l; i++) { candidate = context.include[i]; - if (candidate.nodeType === candidate.ELEMENT_NODE && axe.utils.matchesSelector(candidate, selector)) { + if (candidate.actualNode.nodeType === candidate.actualNode.ELEMENT_NODE && + axe.utils.matchesSelector(candidate.actualNode, selector)) { pushNode(result, [candidate], context); } - pushNode(result, candidate.querySelectorAll(selector), context); + pushNode(result, axe.utils.querySelectorAll(candidate, selector), context); } return result.sort(axe.utils.nodeSorter); diff --git a/lib/rules/definition-list.json b/lib/rules/definition-list.json index 6306f0d2db..74661e1be4 100644 --- a/lib/rules/definition-list.json +++ b/lib/rules/definition-list.json @@ -1,6 +1,7 @@ { "id": "definition-list", - "selector": "dl:not([role])", + "selector": "dl", + "matches": "no-role-matches.js", "tags": [ "cat.structure", "wcag2a", diff --git a/lib/rules/dlitem.json b/lib/rules/dlitem.json index f4676f4253..829f78b1f6 100644 --- a/lib/rules/dlitem.json +++ b/lib/rules/dlitem.json @@ -1,6 +1,7 @@ { "id": "dlitem", - "selector": "dd:not([role]), dt:not([role])", + "selector": "dd, dt", + "matches": "no-role-matches.js", "tags": [ "cat.structure", "wcag2a", diff --git a/lib/rules/frame-title-unique.json b/lib/rules/frame-title-unique.json index 587d4ea9a5..644b6884f8 100644 --- a/lib/rules/frame-title-unique.json +++ b/lib/rules/frame-title-unique.json @@ -1,6 +1,6 @@ { "id": "frame-title-unique", - "selector": "frame[title]:not([title='']), iframe[title]:not([title=''])", + "selector": "frame[title], iframe[title]", "matches": "frame-title-has-text.js", "tags": [ "cat.text-alternatives", diff --git a/lib/rules/label-matches.js b/lib/rules/label-matches.js new file mode 100644 index 0000000000..4feb6d3131 --- /dev/null +++ b/lib/rules/label-matches.js @@ -0,0 +1,14 @@ +// :not([type='hidden']) +// :not([type='image']) +// :not([type='button']) +// :not([type='submit']) +// :not([type='reset']) +if (node.nodeName.toLowerCase() !== 'input') { + return true; +} +var t = node.getAttribute('type').toLowerCase(); +if (t === 'hidden' || t === 'image' || t === 'button' || t === 'submit' || + t === 'reset') { + return false; +} +return true; diff --git a/lib/rules/label-title-only.json b/lib/rules/label-title-only.json index b59b6b57ce..f01910a599 100644 --- a/lib/rules/label-title-only.json +++ b/lib/rules/label-title-only.json @@ -1,6 +1,7 @@ { "id": "label-title-only", - "selector": "input:not([type='hidden']):not([type='image']):not([type='button']):not([type='submit']):not([type='reset']), select, textarea", + "selector": "input, select, textarea", + "matches": "label-matches.js", "enabled": false, "tags": [ "cat.forms", diff --git a/lib/rules/label.json b/lib/rules/label.json index 582c492b69..df36f551a2 100644 --- a/lib/rules/label.json +++ b/lib/rules/label.json @@ -1,6 +1,7 @@ { "id": "label", - "selector": "input:not([type='hidden']):not([type='image']):not([type='button']):not([type='submit']):not([type='reset']), select, textarea", + "selector": "input, select, textarea", + "matches": "label-matches.js", "tags": [ "cat.forms", "wcag2a", diff --git a/lib/rules/link-in-text-block-matches.js b/lib/rules/link-in-text-block-matches.js index 7e2184f8d3..4605c4da5c 100644 --- a/lib/rules/link-in-text-block-matches.js +++ b/lib/rules/link-in-text-block-matches.js @@ -1,5 +1,9 @@ var text = axe.commons.text.sanitize(node.textContent); +var role = node.getAttribute('role'); +if (role && role !== 'link') { + return false; +} if (!text) { return false; } diff --git a/lib/rules/link-in-text-block.json b/lib/rules/link-in-text-block.json index f6c8eaa35e..be9386ec7f 100644 --- a/lib/rules/link-in-text-block.json +++ b/lib/rules/link-in-text-block.json @@ -1,6 +1,6 @@ { "id": "link-in-text-block", - "selector": "a[href]:not([role]), *[role=link]", + "selector": "a[href], *[role=link]", "matches": "link-in-text-block-matches.js", "excludeHidden": false, "enabled": false, diff --git a/lib/rules/link-name.json b/lib/rules/link-name.json index c1d681cea9..63f193ce53 100644 --- a/lib/rules/link-name.json +++ b/lib/rules/link-name.json @@ -1,6 +1,7 @@ { "id": "link-name", - "selector": "a[href]:not([role=\"button\"]), [role=link][href]", + "selector": "a[href], [role=link][href]", + "matches": "role-not-button-matches.js", "tags": [ "cat.name-role-value", "wcag2a", diff --git a/lib/rules/list.json b/lib/rules/list.json index c62b6b0320..752f4705a2 100644 --- a/lib/rules/list.json +++ b/lib/rules/list.json @@ -1,6 +1,7 @@ { "id": "list", - "selector": "ul:not([role]), ol:not([role])", + "selector": "ul, ol", + "matches": "no-role-matches.js", "tags": [ "cat.structure", "wcag2a", diff --git a/lib/rules/listitem.json b/lib/rules/listitem.json index f90b9f7f24..806fb32785 100644 --- a/lib/rules/listitem.json +++ b/lib/rules/listitem.json @@ -1,6 +1,7 @@ { "id": "listitem", - "selector": "li:not([role])", + "selector": "li", + "matches": "no-role-matches.js", "tags": [ "cat.structure", "wcag2a", diff --git a/lib/rules/no-role-matches.js b/lib/rules/no-role-matches.js new file mode 100644 index 0000000000..c7fa630d7c --- /dev/null +++ b/lib/rules/no-role-matches.js @@ -0,0 +1 @@ +return !node.getAttribute('role'); \ No newline at end of file diff --git a/lib/rules/not-html-matches.js b/lib/rules/not-html-matches.js new file mode 100644 index 0000000000..ecea309e48 --- /dev/null +++ b/lib/rules/not-html-matches.js @@ -0,0 +1 @@ +return node.nodeName.toLowerCase() !== 'html'; diff --git a/lib/rules/role-not-button-matches.js b/lib/rules/role-not-button-matches.js new file mode 100644 index 0000000000..41fe6fc535 --- /dev/null +++ b/lib/rules/role-not-button-matches.js @@ -0,0 +1 @@ +return node.getAttribute('role') !== 'button'; diff --git a/lib/rules/valid-lang.json b/lib/rules/valid-lang.json index f6d47c7e32..11d687279d 100644 --- a/lib/rules/valid-lang.json +++ b/lib/rules/valid-lang.json @@ -1,6 +1,7 @@ { "id": "valid-lang", - "selector": "[lang]:not(html), [xml\\:lang]:not(html)", + "selector": "[lang], [xml\\:lang]", + "matches": "not-html-matches.js", "tags": [ "cat.language", "wcag2aa", diff --git a/test/core/base/audit.js b/test/core/base/audit.js index af6a18c948..08ca791c36 100644 --- a/test/core/base/audit.js +++ b/test/core/base/audit.js @@ -368,7 +368,7 @@ describe('Audit', function () { '' + 'FAIL ME'; - a.run({ include: [fixture] }, {}, function (results) { + a.run({ include: [axe.utils.getComposedTree(fixture)[0]] }, {}, function (results) { var expected = [{ id: 'positive1', result: 'inapplicable', @@ -510,7 +510,7 @@ describe('Audit', function () { } }); - a.run({ include: [fixture] }, { + a.run({ include: [axe.utils.getComposedTree(fixture)[0]] }, { runOnly: { 'type': 'rule', 'values': ['throw1'] @@ -537,7 +537,7 @@ describe('Audit', function () { throw new Error('Launch the super sheep!'); } }); - a.run({ include: [fixture] }, { + a.run({ include: [axe.utils.getComposedTree(fixture)[0]] }, { runOnly: { 'type': 'rule', 'values': ['throw1', 'positive1'] @@ -575,7 +575,7 @@ describe('Audit', function () { throw new Error('Launch the super sheep!'); } }); - a.run({ include: [fixture] }, { + a.run({ include: [axe.utils.getComposedTree(fixture)[0]] }, { debug: true, runOnly: { 'type': 'rule', diff --git a/test/core/base/check.js b/test/core/base/check.js index 916fa181a8..fde3d4d8ab 100644 --- a/test/core/base/check.js +++ b/test/core/base/check.js @@ -128,7 +128,7 @@ describe('Check', function () { assert.equal(node, fixture); done(); } - }).run(fixture, {}, noop); + }).run(axe.utils.getComposedTree(fixture)[0], {}, noop); }); diff --git a/test/core/base/context.js b/test/core/base/context.js index 7470617572..a83914d41e 100644 --- a/test/core/base/context.js +++ b/test/core/base/context.js @@ -28,7 +28,7 @@ describe('Context', function() { fixture.innerHTML = '
    '; var result = new Context('#foo'); - assert.deepEqual(result.include, [$id('foo')]); + assert.deepEqual([result.include[0].actualNode], [$id('foo')]); }); it('should accept multiple selectors', function() { @@ -38,7 +38,8 @@ describe('Context', function() { ['#bar'] ]); - assert.deepEqual(result.include, [$id('foo'), $id('bar')]); + assert.deepEqual([result.include[0].actualNode, result.include[1].actualNode], + [$id('foo'), $id('bar')]); }); it('should accept a node reference', function() { @@ -47,43 +48,41 @@ describe('Context', function() { var result = new Context(div); - assert.deepEqual(result.include, [div]); + assert.deepEqual([result.include[0].actualNode], [div]); }); - it('should accept a node reference consisting of nested divs', function() { - var div1 = document.createElement('div'); - var div2 = document.createElement('div'); + it('should accept a node reference consisting of nested divs', function() { + var div1 = document.createElement('div'); + var div2 = document.createElement('div'); - div1.appendChild(div2); - fixture.appendChild(div1); + div1.appendChild(div2); + fixture.appendChild(div1); - var result = new Context(div1); + var result = new Context(div1); - assert.deepEqual(result.include, [div1]); - - }); - - it('should accept a node reference consisting of a form with nested controls', function() { - var form = document.createElement('form'); - var input = document.createElement('input'); + assert.deepEqual([result.include[0].actualNode], [div1]); + }); - form.appendChild(input); - fixture.appendChild(form); + it('should accept a node reference consisting of a form with nested controls', function() { + var form = document.createElement('form'); + var input = document.createElement('input'); - var result = new Context(form); + form.appendChild(input); + fixture.appendChild(form); - assert.deepEqual(result.include, [form]); + var result = new Context(form); - }); + assert.deepEqual([result.include[0].actualNode], [form]); + }); it('should accept an array of node references', function() { fixture.innerHTML = '
    '; var result = new Context([$id('foo'), $id('bar')]); - assert.deepEqual(result.include, [$id('foo'), $id('bar')]); - + assert.deepEqual([result.include[0].actualNode, result.include[1].actualNode], + [$id('foo'), $id('bar')]); }); it('should remove any non-matched reference', function() { @@ -95,8 +94,8 @@ describe('Context', function() { ['#bar'] ]); - assert.deepEqual(result.include, [$id('foo'), $id('bar')]); - + assert.deepEqual(result.include.map(function (n) { return n.actualNode; }), + [$id('foo'), $id('bar')]); }); it('should remove any null reference', function() { @@ -104,7 +103,8 @@ describe('Context', function() { var result = new Context([$id('foo'), $id('bar'), null]); - assert.deepEqual(result.include, [$id('foo'), $id('bar')]); + assert.deepEqual(result.include.map(function (n) { return n.actualNode; }), + [$id('foo'), $id('bar')]); }); @@ -119,7 +119,8 @@ describe('Context', function() { ['#bar'], div ]); - assert.deepEqual(result.include, [$id('foo'), $id('bar'), $id('baz')]); + assert.deepEqual(result.include.map(function (n) { return n.actualNode; }), + [$id('foo'), $id('bar'), $id('baz')]); }); @@ -134,7 +135,8 @@ describe('Context', function() { var result = new Context($test); - assert.deepEqual(result.include, [$id('foo'), $id('bar'), $id('baz')]); + assert.deepEqual(result.include.map(function (n) { return n.actualNode; }), + [$id('foo'), $id('bar'), $id('baz')]); }); @@ -333,8 +335,8 @@ describe('Context', function() { include: ['#fixture'], exclude: ['#mocha'] }), { - include: [document.getElementById('fixture')], - exclude: [document.getElementById('mocha')], + include: [axe.utils.getComposedTree(document.getElementById('fixture'))[0]], + exclude: [axe.utils.getComposedTree(document.getElementById('mocha'))[0]], initiator: true, page: false, frames: [] @@ -347,7 +349,7 @@ describe('Context', function() { include: ['#fixture', '#monkeys'], exclude: ['#bananas'] }), { - include: [document.getElementById('fixture')], + include: [axe.utils.getComposedTree(document.getElementById('fixture'))[0]], exclude: [], initiator: true, page: false, @@ -359,7 +361,7 @@ describe('Context', function() { var result = new Context(); assert.lengthOf(result.include, 1); - assert.equal(result.include[0], document); + assert.equal(result.include[0].actualNode, document.documentElement); assert.lengthOf(result.exclude, 0); @@ -372,10 +374,10 @@ describe('Context', function() { it('should default include to document', function () { var result = new Context({ exclude: ['#fixture'] }); assert.lengthOf(result.include, 1); - assert.equal(result.include[0], document); + assert.equal(result.include[0].actualNode, document.documentElement); assert.lengthOf(result.exclude, 1); - assert.equal(result.exclude[0], $id('fixture')); + assert.equal(result.exclude[0].actualNode, $id('fixture')); assert.isTrue(result.initiator); assert.isTrue(result.page); @@ -387,7 +389,7 @@ describe('Context', function() { it('should default empty include to document', function () { var result = new Context({ include: [], exclude: [] }); assert.lengthOf(result.include, 1); - assert.equal(result.include[0], document); + assert.equal(result.include[0].actualNode, document.documentElement); }); }); @@ -399,7 +401,7 @@ describe('Context', function() { initiator: false }); assert.lengthOf(result.include, 1); - assert.equal(result.include[0], document); + assert.equal(result.include[0].actualNode, document.documentElement); assert.lengthOf(result.exclude, 0); @@ -418,7 +420,7 @@ describe('Context', function() { var result = new Context(spec); assert.lengthOf(result.include, 1); - assert.equal(result.include[0], spec); + assert.equal(result.include[0].actualNode, spec.documentElement); assert.lengthOf(result.exclude, 0); diff --git a/test/core/base/rule.js b/test/core/base/rule.js index a4e1d6c828..56d2caafc7 100644 --- a/test/core/base/rule.js +++ b/test/core/base/rule.js @@ -31,17 +31,17 @@ describe('Rule', function() { selector: '#monkeys' }), nodes = rule.gather({ - include: [fixture], + include: [axe.utils.getComposedTree(fixture)[0]], exclude: [], frames: [] }); assert.lengthOf(nodes, 1); - assert.equal(nodes[0], node); + assert.equal(nodes[0].actualNode, node); node.id = 'bananas'; nodes = rule.gather({ - include: [fixture], + include: [axe.utils.getComposedTree(fixture)[0]], exclude: [], frames: [] }); @@ -54,7 +54,7 @@ describe('Rule', function() { selector: 'div' }), result = rule.gather({ - include: [fixture], + include: [axe.utils.getComposedTree(fixture)[0]], exclude: [], frames: [] }); @@ -70,10 +70,10 @@ describe('Rule', function() { selector: 'div' }), nodes = rule.gather({ - include: [document.getElementById('fixture').firstChild] + include: [axe.utils.getComposedTree(document.getElementById('fixture').firstChild)[0]] }); - assert.deepEqual(nodes, [node]); + assert.deepEqual(nodes.map(function (n) {return n.actualNode;}), [node]); }); it('should default to all nodes if selector is not specified', function() { @@ -90,18 +90,19 @@ describe('Rule', function() { var rule = new Rule({}), result = rule.gather({ - include: [document.getElementById('fixture')] + include: [axe.utils.getComposedTree(document.getElementById('fixture'))[0]] }); assert.lengthOf(result, 3); - assert.sameMembers(result, nodes); + assert.sameMembers(result.map(function (n) { return n.actualNode; }), + nodes); }); it('should exclude hidden elements', function() { fixture.innerHTML = '
    HEHEHE
    '; var rule = new Rule({}), result = rule.gather({ - include: [document.getElementById('fixture').firstChild] + include: [axe.utils.getComposedTree(document.getElementById('fixture').firstChild)[0]] }); assert.lengthOf(result, 0); @@ -113,10 +114,10 @@ describe('Rule', function() { excludeHidden: false }), result = rule.gather({ - include: [document.getElementById('fixture').firstChild] + include: [axe.utils.getComposedTree(document.getElementById('fixture').firstChild)[0]] }); - assert.deepEqual(result, [fixture.firstChild]); + assert.deepEqual(result.map(function (n) { return n.actualNode; }), [fixture.firstChild]); }); }); describe('run', function() { @@ -137,7 +138,7 @@ describe('Rule', function() { }); rule.run({ - include: [div] + include: [axe.utils.getComposedTree(div)[0]] }, {}, function() { assert.isTrue(success); done(); @@ -157,7 +158,7 @@ describe('Rule', function() { }); rule.run({ - include: [div] + include: [axe.utils.getComposedTree(div)[0]] }, {}, isNotCalled, function() { assert.isFalse(success); done(); @@ -181,7 +182,7 @@ describe('Rule', function() { }); rule.run({ - include: [fixture] + include: [axe.utils.getComposedTree(fixture)[0]] }, {}, function() { assert.isTrue(success); done(); @@ -206,7 +207,7 @@ describe('Rule', function() { }); rule.run({ - include: [fixture] + include: [axe.utils.getComposedTree(fixture)[0]] }, {}, function() { assert.isTrue(success); done(); @@ -231,7 +232,7 @@ describe('Rule', function() { }, isNotCalled); rule.run({ - include: [fixture] + include: [axe.utils.getComposedTree(fixture)[0]] }, {}, function() { assert.isTrue(success); done(); @@ -264,7 +265,7 @@ describe('Rule', function() { } }); rule.run({ - include: [document] + include: [axe.utils.getComposedTree(document)[0]] }, options, function() { done(); }, isNotCalled); @@ -309,7 +310,7 @@ describe('Rule', function() { } }); rule.run({ - include: [document] + include: [axe.utils.getComposedTree(document)[0]] }, options, function() { done(); }, isNotCalled); @@ -328,7 +329,7 @@ describe('Rule', function() { } }); rule.run({ - include: [document] + include: [axe.utils.getComposedTree(document)[0]] }, {}, function(r) { assert.lengthOf(r.nodes, 0); }, isNotCalled); @@ -369,7 +370,7 @@ describe('Rule', function() { } }); rule.run({ - include: [fixture] + include: [axe.utils.getComposedTree(fixture)[0]] }, {}, function() { assert.isTrue(isDqElementCalled); done(); @@ -392,7 +393,7 @@ describe('Rule', function() { } }); rule.run({ - include: [fixture] + include: [axe.utils.getComposedTree(fixture)[0]] }, {}, function() { assert.isFalse(isDqElementCalled); done(); @@ -414,7 +415,7 @@ describe('Rule', function() { } }); rule.run({ - include: [fixture] + include: [axe.utils.getComposedTree(fixture)[0]] }, {}, function() { assert.isTrue(isDqElementCalled); done(); @@ -434,7 +435,7 @@ describe('Rule', function() { } }); rule.run({ - include: [fixture] + include: [axe.utils.getComposedTree(fixture)[0]] }, {}, function() { assert.isFalse(isDqElementCalled); done(); @@ -457,7 +458,7 @@ describe('Rule', function() { }); rule.run({ - include: [fixture] + include: [axe.utils.getComposedTree(fixture)[0]] }, {}, noop, function(err) { assert.equal(err.message, 'Holy hand grenade'); done(); @@ -479,7 +480,7 @@ describe('Rule', function() { }); rule.run({ - include: [fixture] + include: [axe.utils.getComposedTree(fixture)[0]] }, {}, noop, function(err) { assert.equal(err.message, 'your reality'); done(); @@ -503,7 +504,7 @@ describe('Rule', function() { }] }); rule.run({ - include: document + include: axe.utils.getComposedTree(document)[0] }, {}, noop, isNotCalled); assert.isTrue(success); @@ -520,7 +521,7 @@ describe('Rule', function() { }] }); rule.run({ - include: document + include: axe.utils.getComposedTree(document)[0] }, {}, function() { success = true; }, isNotCalled); diff --git a/test/core/public/run-rules.js b/test/core/public/run-rules.js index be68a729b6..bd147aa5d8 100644 --- a/test/core/public/run-rules.js +++ b/test/core/public/run-rules.js @@ -314,7 +314,7 @@ describe('runRules', function () { any: ['has-target'] }, { id: 'first-div', - selector: 'div:not([id=fixture])', + selector: 'div#fixture div', any: ['first-div'], }], checks: [{ diff --git a/test/core/utils/contains.js b/test/core/utils/contains.js index cb97255f85..c0e2bf8d67 100644 --- a/test/core/utils/contains.js +++ b/test/core/utils/contains.js @@ -9,16 +9,18 @@ describe('axe.utils.contains', function () { it('should first check contains', function () { var success = false, - node2 = 'not really a node but it doesnt matter', + node2 = { actualNode: 'not really a node but it doesnt matter' }, node1 = { - contains: function (n2) { - success = true; - assert.equal(n2, node2); - }, - compareDocumentPosition: function () { - success = false; - assert.ok(false, 'should not be called'); + actualNode: { + contains: function (n2) { + success = true; + assert.deepEqual(n2, node2.actualNode); + }, + compareDocumentPosition: function () { + success = false; + assert.ok(false, 'should not be called'); + } } }; @@ -28,11 +30,13 @@ describe('axe.utils.contains', function () { it('should fallback to compareDocumentPosition', function () { var success = false, - node2 = 'not really a node but it doesnt matter', + node2 = { actualNode: 'not really a node but it doesnt matter' }, node1 = { - compareDocumentPosition: function (n2) { - success = true; - assert.equal(n2, node2); + actualNode: { + compareDocumentPosition: function (n2) { + success = true; + assert.deepEqual(n2, node2.actualNode); + } } }; @@ -41,10 +45,12 @@ describe('axe.utils.contains', function () { }); it('should compareDocumentPosition against bitwise & 16', function () { - var node2 = 'not really a node but it doesnt matter', + var node2 = { actualNode: 'not really a node but it doesnt matter' }, node1 = { - compareDocumentPosition: function () { - return 20; + actualNode: { + compareDocumentPosition: function () { + return 20; + } } }; @@ -53,8 +59,8 @@ describe('axe.utils.contains', function () { it('should work', function () { fixture.innerHTML = '
    '; - var inner = document.getElementById('inner'); - var outer = document.getElementById('outer'); + var inner = axe.utils.getComposedTree(document.getElementById('inner'))[0]; + var outer = axe.utils.getComposedTree(document.getElementById('outer'))[0]; assert.isTrue(axe.utils.contains(outer, inner)); assert.isFalse(axe.utils.contains(inner, outer)); diff --git a/test/core/utils/node-sorter.js b/test/core/utils/node-sorter.js index 1b50568e7c..9f4d9f557c 100644 --- a/test/core/utils/node-sorter.js +++ b/test/core/utils/node-sorter.js @@ -15,30 +15,30 @@ describe('axe.utils.nodeSorter', function () { it('should return -1 if a comes before b', function () { fixture.innerHTML = '
    '; - assert.equal(axe.utils.nodeSorter($id('a'), $id('b')), -1); + assert.equal(axe.utils.nodeSorter({ actualNode: $id('a') }, { actualNode: $id('b') }), -1); }); it('should return -1 if a comes before b - nested', function () { fixture.innerHTML = '
    '; - assert.equal(axe.utils.nodeSorter($id('a'), $id('b')), -1); + assert.equal(axe.utils.nodeSorter({ actualNode: $id('a') }, { actualNode: $id('b') }), -1); }); it('should return 1 if b comes before a', function () { fixture.innerHTML = '
    '; - assert.equal(axe.utils.nodeSorter($id('a'), $id('b')), 1); + assert.equal(axe.utils.nodeSorter({ actualNode: $id('a') }, { actualNode: $id('b') }), 1); }); it('should return 1 if b comes before a - nested', function () { fixture.innerHTML = '
    '; - assert.equal(axe.utils.nodeSorter($id('a'), $id('b')), 1); + assert.equal(axe.utils.nodeSorter({ actualNode: $id('a') }, { actualNode: $id('b') }), 1); }); it('should return 0 if a === b', function () { fixture.innerHTML = '
    '; - assert.equal(axe.utils.nodeSorter($id('a'), $id('a')), 0); + assert.equal(axe.utils.nodeSorter({ actualNode: $id('a') }, { actualNode: $id('a') }), 0); }); }); \ No newline at end of file diff --git a/test/core/utils/qsa.js b/test/core/utils/qsa.js index 3a7f4a1760..9d0338a604 100644 --- a/test/core/utils/qsa.js +++ b/test/core/utils/qsa.js @@ -4,6 +4,7 @@ function Vnode (nodeName, className, attributes, id) { this.id = id; this.attributes = attributes || []; this.className = className; + this.nodeType = 1; } Vnode.prototype.getAttribute = function (att) { @@ -11,7 +12,7 @@ Vnode.prototype.getAttribute = function (att) { var attribute = this.attributes.find(function (item) { return item.key === att; }); - return attribute ? attribute.value : ''; + return attribute ? attribute.value : null; }; describe('axe.utils.querySelectorAll', function () { diff --git a/test/core/utils/select.js b/test/core/utils/select.js index 57456931df..e7f8f8fb93 100644 --- a/test/core/utils/select.js +++ b/test/core/utils/select.js @@ -28,9 +28,9 @@ describe('axe.utils.select', function () { div.id = 'monkeys'; fixture.appendChild(div); - var result = axe.utils.select('#monkeys', { include: [document] }); + var result = axe.utils.select('#monkeys', { include: [axe.utils.getComposedTree(document)[0]] }); - assert.equal(result[0], div); + assert.equal(result[0].actualNode, div); }); @@ -41,10 +41,10 @@ describe('axe.utils.select', function () { fixture.innerHTML = '
    '; var result = axe.utils.select('.bananas', { - include: [$id('monkeys')] + include: [axe.utils.getComposedTree($id('monkeys'))[0]] }); - assert.deepEqual(result, [$id('bananas')]); + assert.deepEqual([result[0].actualNode], [$id('bananas')]); }); @@ -52,8 +52,8 @@ describe('axe.utils.select', function () { fixture.innerHTML = '
    '; var result = axe.utils.select('.bananas', { - include: [$id('fixture')], - exclude: [$id('monkeys')] + include: [axe.utils.getComposedTree($id('fixture'))[0]], + exclude: [axe.utils.getComposedTree($id('monkeys'))[0]] }); assert.deepEqual(result, []); @@ -73,8 +73,10 @@ describe('axe.utils.select', function () { var result = axe.utils.select('.bananas', { - include: [$id('include1'), $id('include2')], - exclude: [$id('exclude1'), $id('exclude2')] + include: [axe.utils.getComposedTree($id('include1'))[0], + axe.utils.getComposedTree($id('include2'))[0]], + exclude: [axe.utils.getComposedTree($id('exclude1'))[0], + axe.utils.getComposedTree($id('exclude2'))[0]] }); assert.deepEqual(result, []); @@ -96,11 +98,14 @@ describe('axe.utils.select', function () { var result = axe.utils.select('.bananas', { - include: [$id('include3'), $id('include2'), $id('include1')], - exclude: [$id('exclude1'), $id('exclude2')] + include: [axe.utils.getComposedTree($id('include3'))[0], + axe.utils.getComposedTree($id('include2'))[0], + axe.utils.getComposedTree($id('include1'))[0]], + exclude: [axe.utils.getComposedTree($id('exclude1'))[0], + axe.utils.getComposedTree($id('exclude2'))[0]] }); - assert.deepEqual(result, [$id('bananas')]); + assert.deepEqual([result[0].actualNode], [$id('bananas')]); }); @@ -109,10 +114,13 @@ describe('axe.utils.select', function () { it('should only contain unique elements', function () { fixture.innerHTML = '
    '; - var result = axe.utils.select('.bananas', { include: [$id('fixture'), $id('monkeys')] }); + var result = axe.utils.select('.bananas', { + include: [axe.utils.getComposedTree($id('fixture'))[0], + axe.utils.getComposedTree($id('monkeys'))[0]] + }); assert.lengthOf(result, 1); - assert.equal(result[0], $id('bananas')); + assert.equal(result[0].actualNode, $id('bananas')); }); @@ -120,10 +128,11 @@ describe('axe.utils.select', function () { fixture.innerHTML = '
    ' + '
    '; - var result = axe.utils.select('.bananas', { include: [$id('two'), $id('one')] }); - - assert.deepEqual(result, [$id('target1'), $id('target2')]); + var result = axe.utils.select('.bananas', { include: [axe.utils.getComposedTree($id('two'))[0], + axe.utils.getComposedTree($id('one'))[0]] }); + assert.deepEqual(result.map(function (n) { return n.actualNode; }), + [$id('target1'), $id('target2')]); }); From 655eacba79e48783d6b2f46ed273850148e1e130 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Mon, 27 Mar 2017 18:40:57 -0400 Subject: [PATCH 008/142] implement :not --- Gruntfile.js | 14 +- lib/core/utils/css-parser.js | 624 ++++++++++++++++++++++++++++++++ lib/core/utils/merge-results.js | 3 +- lib/core/utils/qsa.js | 173 ++++++++- lib/core/utils/slick-parser.js | 219 ----------- test/core/public/run-rules.js | 2 +- test/core/utils/qsa.js | 36 ++ 7 files changed, 834 insertions(+), 237 deletions(-) create mode 100644 lib/core/utils/css-parser.js delete mode 100644 lib/core/utils/slick-parser.js diff --git a/Gruntfile.js b/Gruntfile.js index 8eb2eeff27..600a23d31a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -54,14 +54,14 @@ module.exports = function (grunt) { ] } }, - retire: { + retire: { options: { /** list of files to ignore **/ ignorefile: '.retireignore.json' //or '.retireignore.json' }, js: ['lib/*.js'], /** Which js-files to scan. **/ node: ['./'] /** Which node directories to scan (containing package.json). **/ - }, + }, clean: ['dist', 'tmp', 'axe.js', 'axe.*.js'], babel: { options: { @@ -94,15 +94,15 @@ module.exports = function (grunt) { }, concat: { engine: { + options: { + process: true + }, coreFiles: [ 'tmp/core/index.js', 'tmp/core/*/index.js', 'tmp/core/**/index.js', 'tmp/core/**/*.js' ], - options: { - process: true - }, files: langs.map(function (lang, i) { return { src: [ @@ -112,7 +112,7 @@ module.exports = function (grunt) { '<%= configure.rules.files[' + i + '].dest.auto %>', 'lib/outro.stub' ], - dest: 'axe' + lang + '.js', + dest: 'axe' + lang + '.js' }; }) }, @@ -193,7 +193,7 @@ module.exports = function (grunt) { bracketize: true, quote_style: 1 }, - preserveComments: 'some' + preserveComments: 'all' } }, minify: { diff --git a/lib/core/utils/css-parser.js b/lib/core/utils/css-parser.js new file mode 100644 index 0000000000..2915734413 --- /dev/null +++ b/lib/core/utils/css-parser.js @@ -0,0 +1,624 @@ +/* jshint ignore:start */ +(function (axe) { +/*! + * The copyright below covers the code within this function block only + * + * Copyright (c) 2013 Dulin Marat + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + function CssSelectorParser() { + this.pseudos = {}; + this.attrEqualityMods = {}; + this.ruleNestingOperators = {}; + this.substitutesEnabled = false; + } + + CssSelectorParser.prototype.registerSelectorPseudos = function(name) { + for (var j = 0, len = arguments.length; j < len; j++) { + name = arguments[j]; + this.pseudos[name] = 'selector'; + } + return this; + }; + + CssSelectorParser.prototype.unregisterSelectorPseudos = function(name) { + for (var j = 0, len = arguments.length; j < len; j++) { + name = arguments[j]; + delete this.pseudos[name]; + } + return this; + }; + + CssSelectorParser.prototype.registerNumericPseudos = function(name) { + for (var j = 0, len = arguments.length; j < len; j++) { + name = arguments[j]; + this.pseudos[name] = 'numeric'; + } + return this; + }; + + CssSelectorParser.prototype.unregisterNumericPseudos = function(name) { + for (var j = 0, len = arguments.length; j < len; j++) { + name = arguments[j]; + delete this.pseudos[name]; + } + return this; + }; + + CssSelectorParser.prototype.registerNestingOperators = function(operator) { + for (var j = 0, len = arguments.length; j < len; j++) { + operator = arguments[j]; + this.ruleNestingOperators[operator] = true; + } + return this; + }; + + CssSelectorParser.prototype.unregisterNestingOperators = function(operator) { + for (var j = 0, len = arguments.length; j < len; j++) { + operator = arguments[j]; + delete this.ruleNestingOperators[operator]; + } + return this; + }; + + CssSelectorParser.prototype.registerAttrEqualityMods = function(mod) { + for (var j = 0, len = arguments.length; j < len; j++) { + mod = arguments[j]; + this.attrEqualityMods[mod] = true; + } + return this; + }; + + CssSelectorParser.prototype.unregisterAttrEqualityMods = function(mod) { + for (var j = 0, len = arguments.length; j < len; j++) { + mod = arguments[j]; + delete this.attrEqualityMods[mod]; + } + return this; + }; + + CssSelectorParser.prototype.enableSubstitutes = function() { + this.substitutesEnabled = true; + return this; + }; + + CssSelectorParser.prototype.disableSubstitutes = function() { + this.substitutesEnabled = false; + return this; + }; + + function isIdentStart(c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c === '-') || (c === '_'); + } + + function isIdent(c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '-' || c === '_'; + } + + function isHex(c) { + return (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || (c >= '0' && c <= '9'); + } + + function isDecimal(c) { + return c >= '0' && c <= '9'; + } + + function isAttrMatchOperator(chr) { + return chr === '=' || chr === '^' || chr === '$' || chr === '*' || chr === '~'; + } + + var identSpecialChars = { + '!': true, + '"': true, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '.': true, + '/': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + '[': true, + '\\': true, + ']': true, + '^': true, + '`': true, + '{': true, + '|': true, + '}': true, + '~': true + }; + + var strReplacementsRev = { + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\f': '\\f', + '\v': '\\v' + }; + + var singleQuoteEscapeChars = { + n: '\n', + r: '\r', + t: '\t', + f: '\f', + '\\': '\\', + '\'': '\'' + }; + + var doubleQuotesEscapeChars = { + n: '\n', + r: '\r', + t: '\t', + f: '\f', + '\\': '\\', + '"': '"' + }; + + function ParseContext(str, pos, pseudos, attrEqualityMods, ruleNestingOperators, substitutesEnabled) { + var chr, getIdent, getStr, l, skipWhitespace; + l = str.length; + chr = null; + getStr = function(quote, escapeTable) { + var esc, hex, result; + result = ''; + pos++; + chr = str.charAt(pos); + while (pos < l) { + if (chr === quote) { + pos++; + return result; + } else if (chr === '\\') { + pos++; + chr = str.charAt(pos); + if (chr === quote) { + result += quote; + } else if (esc = escapeTable[chr]) { + result += esc; + } else if (isHex(chr)) { + hex = chr; + pos++; + chr = str.charAt(pos); + while (isHex(chr)) { + hex += chr; + pos++; + chr = str.charAt(pos); + } + if (chr === ' ') { + pos++; + chr = str.charAt(pos); + } + result += String.fromCharCode(parseInt(hex, 16)); + continue; + } else { + result += chr; + } + } else { + result += chr; + } + pos++; + chr = str.charAt(pos); + } + return result; + }; + getIdent = function() { + var result = ''; + chr = str.charAt(pos); + while (pos < l) { + if (isIdent(chr)) { + result += chr; + } else if (chr === '\\') { + pos++; + if (pos >= l) { + throw Error('Expected symbol but end of file reached.'); + } + chr = str.charAt(pos); + if (identSpecialChars[chr]) { + result += chr; + } else if (isHex(chr)) { + var hex = chr; + pos++; + chr = str.charAt(pos); + while (isHex(chr)) { + hex += chr; + pos++; + chr = str.charAt(pos); + } + if (chr === ' ') { + pos++; + chr = str.charAt(pos); + } + result += String.fromCharCode(parseInt(hex, 16)); + continue; + } else { + result += chr; + } + } else { + return result; + } + pos++; + chr = str.charAt(pos); + } + return result; + }; + skipWhitespace = function() { + chr = str.charAt(pos); + var result = false; + while (chr === ' ' || chr === "\t" || chr === "\n" || chr === "\r" || chr === "\f") { + result = true; + pos++; + chr = str.charAt(pos); + } + return result; + }; + this.parse = function() { + var res = this.parseSelector(); + if (pos < l) { + throw Error('Rule expected but "' + str.charAt(pos) + '" found.'); + } + return res; + }; + this.parseSelector = function() { + var res; + var selector = res = this.parseSingleSelector(); + chr = str.charAt(pos); + while (chr === ',') { + pos++; + skipWhitespace(); + if (res.type !== 'selectors') { + res = { + type: 'selectors', + selectors: [selector] + }; + } + selector = this.parseSingleSelector(); + if (!selector) { + throw Error('Rule expected after ",".'); + } + res.selectors.push(selector); + } + return res; + }; + + this.parseSingleSelector = function() { + skipWhitespace(); + var selector = { + type: 'ruleSet' + }; + var rule = this.parseRule(); + if (!rule) { + return null; + } + var currentRule = selector; + while (rule) { + rule.type = 'rule'; + currentRule.rule = rule; + currentRule = rule; + skipWhitespace(); + chr = str.charAt(pos); + if (pos >= l || chr === ',' || chr === ')') { + break; + } + if (ruleNestingOperators[chr]) { + var op = chr; + pos++; + skipWhitespace(); + rule = this.parseRule(); + if (!rule) { + throw Error('Rule expected after "' + op + '".'); + } + rule.nestingOperator = op; + } else { + rule = this.parseRule(); + if (rule) { + rule.nestingOperator = null; + } + } + } + return selector; + }; + + this.parseRule = function() { + var rule = null; + while (pos < l) { + chr = str.charAt(pos); + if (chr === '*') { + pos++; + (rule = rule || {}).tagName = '*'; + } else if (isIdentStart(chr) || chr === '\\') { + (rule = rule || {}).tagName = getIdent(); + } else if (chr === '.') { + pos++; + rule = rule || {}; + (rule.classNames = rule.classNames || []).push(getIdent()); + } else if (chr === '#') { + pos++; + (rule = rule || {}).id = getIdent(); + } else if (chr === '[') { + pos++; + skipWhitespace(); + var attr = { + name: getIdent() + }; + skipWhitespace(); + if (chr === ']') { + pos++; + } else { + var operator = ''; + if (attrEqualityMods[chr]) { + operator = chr; + pos++; + chr = str.charAt(pos); + } + if (pos >= l) { + throw Error('Expected "=" but end of file reached.'); + } + if (chr !== '=') { + throw Error('Expected "=" but "' + chr + '" found.'); + } + attr.operator = operator + '='; + pos++; + skipWhitespace(); + var attrValue = ''; + attr.valueType = 'string'; + if (chr === '"') { + attrValue = getStr('"', doubleQuotesEscapeChars); + } else if (chr === '\'') { + attrValue = getStr('\'', singleQuoteEscapeChars); + } else if (substitutesEnabled && chr === '$') { + pos++; + attrValue = getIdent(); + attr.valueType = 'substitute'; + } else { + while (pos < l) { + if (chr === ']') { + break; + } + attrValue += chr; + pos++; + chr = str.charAt(pos); + } + attrValue = attrValue.trim(); + } + skipWhitespace(); + if (pos >= l) { + throw Error('Expected "]" but end of file reached.'); + } + if (chr !== ']') { + throw Error('Expected "]" but "' + chr + '" found.'); + } + pos++; + attr.value = attrValue; + } + rule = rule || {}; + (rule.attrs = rule.attrs || []).push(attr); + } else if (chr === ':') { + pos++; + var pseudoName = getIdent(); + var pseudo = { + name: pseudoName + }; + if (chr === '(') { + pos++; + var value = ''; + skipWhitespace(); + if (pseudos[pseudoName] === 'selector') { + pseudo.valueType = 'selector'; + value = this.parseSelector(); + } else { + pseudo.valueType = pseudos[pseudoName] || 'string'; + if (chr === '"') { + value = getStr('"', doubleQuotesEscapeChars); + } else if (chr === '\'') { + value = getStr('\'', singleQuoteEscapeChars); + } else if (substitutesEnabled && chr === '$') { + pos++; + value = getIdent(); + pseudo.valueType = 'substitute'; + } else { + while (pos < l) { + if (chr === ')') { + break; + } + value += chr; + pos++; + chr = str.charAt(pos); + } + value = value.trim(); + } + skipWhitespace(); + } + if (pos >= l) { + throw Error('Expected ")" but end of file reached.'); + } + if (chr !== ')') { + throw Error('Expected ")" but "' + chr + '" found.'); + } + pos++; + pseudo.value = value; + } + rule = rule || {}; + (rule.pseudos = rule.pseudos || []).push(pseudo); + } else { + break; + } + } + return rule; + }; + return this; + } + + CssSelectorParser.prototype.parse = function(str) { + var context = new ParseContext( + str, + 0, + this.pseudos, + this.attrEqualityMods, + this.ruleNestingOperators, + this.substitutesEnabled + ); + return context.parse(); + }; + + CssSelectorParser.prototype.escapeIdentifier = function(s) { + var result = ''; + var i = 0; + var len = s.length; + while (i < len) { + var chr = s.charAt(i); + if (identSpecialChars[chr]) { + result += '\\' + chr; + } else { + if ( + !( + chr === '_' || chr === '-' || + (chr >= 'A' && chr <= 'Z') || + (chr >= 'a' && chr <= 'z') || + (i !== 0 && chr >= '0' && chr <= '9') + ) + ) { + var charCode = chr.charCodeAt(0); + if ((charCode & 0xF800) === 0xD800) { + var extraCharCode = s.charCodeAt(i++); + if ((charCode & 0xFC00) !== 0xD800 || (extraCharCode & 0xFC00) !== 0xDC00) { + throw Error('UCS-2(decode): illegal sequence'); + } + charCode = ((charCode & 0x3FF) << 10) + (extraCharCode & 0x3FF) + 0x10000; + } + result += '\\' + charCode.toString(16) + ' '; + } else { + result += chr; + } + } + i++; + } + return result; + }; + + CssSelectorParser.prototype.escapeStr = function(s) { + var result = ''; + var i = 0; + var len = s.length; + var chr, replacement; + while (i < len) { + chr = s.charAt(i); + if (chr === '"') { + chr = '\\"'; + } else if (chr === '\\') { + chr = '\\\\'; + } else if (replacement = strReplacementsRev[chr]) { + chr = replacement; + } + result += chr; + i++; + } + return "\"" + result + "\""; + }; + + CssSelectorParser.prototype.render = function(path) { + return this._renderEntity(path).trim(); + }; + + CssSelectorParser.prototype._renderEntity = function(entity) { + var currentEntity, parts, res; + res = ''; + switch (entity.type) { + case 'ruleSet': + currentEntity = entity.rule; + parts = []; + while (currentEntity) { + if (currentEntity.nestingOperator) { + parts.push(currentEntity.nestingOperator); + } + parts.push(this._renderEntity(currentEntity)); + currentEntity = currentEntity.rule; + } + res = parts.join(' '); + break; + case 'selectors': + res = entity.selectors.map(this._renderEntity, this).join(', '); + break; + case 'rule': + if (entity.tagName) { + if (entity.tagName === '*') { + res = '*'; + } else { + res = this.escapeIdentifier(entity.tagName); + } + } + if (entity.id) { + res += "#" + this.escapeIdentifier(entity.id); + } + if (entity.classNames) { + res += entity.classNames.map(function(cn) { + return "." + (this.escapeIdentifier(cn)); + }, this).join(''); + } + if (entity.attrs) { + res += entity.attrs.map(function(attr) { + if (attr.operator) { + if (attr.valueType === 'substitute') { + return "[" + this.escapeIdentifier(attr.name) + attr.operator + "$" + attr.value + "]"; + } else { + return "[" + this.escapeIdentifier(attr.name) + attr.operator + this.escapeStr(attr.value) + "]"; + } + } else { + return "[" + this.escapeIdentifier(attr.name) + "]"; + } + }, this).join(''); + } + if (entity.pseudos) { + res += entity.pseudos.map(function(pseudo) { + if (pseudo.valueType) { + if (pseudo.valueType === 'selector') { + return ":" + this.escapeIdentifier(pseudo.name) + "(" + this._renderEntity(pseudo.value) + ")"; + } else if (pseudo.valueType === 'substitute') { + return ":" + this.escapeIdentifier(pseudo.name) + "($" + pseudo.value + ")"; + } else if (pseudo.valueType === 'numeric') { + return ":" + this.escapeIdentifier(pseudo.name) + "(" + pseudo.value + ")"; + } else { + return ":" + this.escapeIdentifier(pseudo.name) + "(" + this.escapeIdentifier(pseudo.value) + ")"; + } + } else { + return ":" + this.escapeIdentifier(pseudo.name); + } + }, this).join(''); + } + break; + default: + throw Error('Unknown entity type: "' + entity.type(+'".')); + } + return res; + }; + + var parser = new CssSelectorParser(); + parser.registerNestingOperators('>'); + axe.utils.cssParser = parser; +})(axe); diff --git a/lib/core/utils/merge-results.js b/lib/core/utils/merge-results.js index 3651e6a4ed..fa498ca52a 100644 --- a/lib/core/utils/merge-results.js +++ b/lib/core/utils/merge-results.js @@ -43,7 +43,8 @@ function spliceNodes(target, to) { sorterResult, t; for (var i = 0, l = target.length; i < l; i++) { t = target[i].node; - sorterResult = axe.utils.nodeSorter(t.element, firstFromFrame.element); + sorterResult = axe.utils.nodeSorter({ actualNode: t.element }, + { actualNode: firstFromFrame.element }); if (sorterResult > 0 || (sorterResult === 0 && firstFromFrame.selector.length < t.selector.length)) { target.splice.apply(target, [i, 0].concat(to)); return; diff --git a/lib/core/utils/qsa.js b/lib/core/utils/qsa.js index 3690e78216..780be20d6c 100644 --- a/lib/core/utils/qsa.js +++ b/lib/core/utils/qsa.js @@ -3,6 +3,10 @@ * supports shadowDOM */ +// 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 (node, exp) { @@ -26,18 +30,29 @@ function matchesId (node, exp) { return !exp.id || node.id === exp.id; } +function matchesPseudos (target, exp) { + + if (!exp.pseudos || exp.pseudos.reduce((result, pseudo) => { + if (pseudo.name === 'not') { + return result && !matchExpressions([target], pseudo.expressions, false).length; + } + throw new Error('the pseudo selector ' + pseudo.name + ' has not yet been implemented'); + }, true)) { + return true; + } + return false; +} + function matchSelector (targets, exp, recurse) { var result = []; - if (exp.pseudos) { - throw new Error('matchSelector does not support pseudo selector: ' + exp.pseudos[0].key); - } targets = Array.isArray(targets) ? targets : [targets]; targets.forEach((target) => { if (matchesTag(target.actualNode, exp) && matchesClasses(target.actualNode, exp) && matchesAttributes(target.actualNode, exp) && - matchesId(target.actualNode, exp)) { + matchesId(target.actualNode, exp) && + matchesPseudos(target, exp)) { result.push(target); } if (recurse) { @@ -49,21 +64,161 @@ function matchSelector (targets, exp, recurse) { return result; } -axe.utils.querySelectorAll = function (domTree, selector) { - domTree = Array.isArray(domTree) ? domTree : [domTree]; +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) => { + // jshint maxcomplexity:12 + 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.indexOf(attributeValue) > -1; + }; + 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 + }; + }); +} - return axe.utils.cssParser(selector).expressions.reduce((collected, exprArr) => { +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 = axe.utils.cssParser.parse(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 + * + * @param Array{Object} expressions + * @return Array{Object} + * + */ +convertExpressions = function (expressions) { + return expressions.map((exp) => { + var newExp = []; + var rule = exp.rule; + while(rule) { + 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; + }); +}; + +matchExpressions = function (domTree, expressions, recurse) { + return expressions.reduce((collected, exprArr) => { var candidates = domTree; exprArr.forEach((exp, index) => { - var recurse = exp.combinator === '>' ? false : true; + recurse = exp.combinator === '>' ? false : recurse; if ([' ', '>'].indexOf(exp.combinator) === -1) { throw new Error('axe.utils.querySelectorAll does not support the combinator: ' + exp.combinator); } - exp.tag = exp.tag.toLowerCase(); // do this once candidates = candidates.reduce((result, node) => { return result.concat(matchSelector(index ? node.children : node, exp, recurse)); }, []); }); return collected.concat(candidates); }, []); +}; + +axe.utils.querySelectorAll = function (domTree, selector) { + 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); }; \ No newline at end of file diff --git a/lib/core/utils/slick-parser.js b/lib/core/utils/slick-parser.js deleted file mode 100644 index 188ad8fb18..0000000000 --- a/lib/core/utils/slick-parser.js +++ /dev/null @@ -1,219 +0,0 @@ -/* jshint ignore:start */ -/* Copyright Mootools Developers, licensed under the MIT license https://opensource.org/licenses/MIT */ -/* ---- -name: Slick.Parser -description: Standalone CSS3 Selector parser -provides: Slick.Parser -... -*/ -(function (axe) { - var parsed, - separatorIndex, - combinatorIndex, - reversed, - cache = {}, - reverseCache = {}, - reUnescape = /\\/g; - - var parse = function(expression, isReversed){ - if (expression == null) return null; - if (expression.Slick === true) return expression; - expression = ('' + expression).replace(/^\s+|\s+$/g, ''); - reversed = !!isReversed; - var currentCache = (reversed) ? reverseCache : cache; - if (currentCache[expression]) return currentCache[expression]; - parsed = { - Slick: true, - expressions: [], - raw: expression, - reverse: function(){ - return parse(this.raw, true); - } - }; - separatorIndex = -1; - while (expression != (expression = expression.replace(regexp, parser))); - parsed.length = parsed.expressions.length; - return currentCache[parsed.raw] = (reversed) ? reverse(parsed) : parsed; - }; - - var reverseCombinator = function(combinator){ - if (combinator === '!') return ' '; - else if (combinator === ' ') return '!'; - else if ((/^!/).test(combinator)) return combinator.replace(/^!/, ''); - else return '!' + combinator; - }; - - var reverse = function(expression){ - var expressions = expression.expressions; - for (var i = 0; i < expressions.length; i++){ - var exp = expressions[i]; - var last = {parts: [], tag: '*', combinator: reverseCombinator(exp[0].combinator)}; - - for (var j = 0; j < exp.length; j++){ - var cexp = exp[j]; - if (!cexp.reverseCombinator) cexp.reverseCombinator = ' '; - cexp.combinator = cexp.reverseCombinator; - delete cexp.reverseCombinator; - } - - exp.reverse().push(last); - } - return expression; - }; - - var escapeRegExp = (function(){ - // Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan MIT License - var from = /(?=[\-\[\]{}()*+?.\\\^$|,#\s])/g, to = '\\'; - return function(string){ return string.replace(from, to) } - }()) - - var regexp = new RegExp( - /* - #!/usr/bin/env ruby - puts "\t\t" + DATA.read.gsub(/\(\?x\)|\s+#.*$|\s+|\\$|\\n/,'') - __END__ - "(?x)^(?:\ - \\s* ( , ) \\s* # Separator \n\ - | \\s* ( + ) \\s* # Combinator \n\ - | ( \\s+ ) # CombinatorChildren \n\ - | ( + | \\* ) # Tag \n\ - | \\# ( + ) # ID \n\ - | \\. ( + ) # ClassName \n\ - | # Attribute \n\ - \\[ \ - \\s* (+) (?: \ - \\s* ([*^$!~|]?=) (?: \ - \\s* (?:\ - ([\"']?)(.*?)\\9 \ - )\ - ) \ - )? \\s* \ - \\](?!\\]) \n\ - | :+ ( + )(?:\ - \\( (?:\ - (?:([\"'])([^\\12]*)\\12)|((?:\\([^)]+\\)|[^()]*)+)\ - ) \\)\ - )?\ - )" - */ - "^(?:\\s*(,)\\s*|\\s*(+)\\s*|(\\s+)|(+|\\*)|\\#(+)|\\.(+)|\\[\\s*(+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)" - .replace(//, '[' + escapeRegExp(">+~`!@$%^&={}\\;/g, '(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])') - .replace(//g, '(?:[:\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])') - ); - - function parser( - rawMatch, - - separator, - combinator, - combinatorChildren, - - tagName, - id, - className, - - attributeKey, - attributeOperator, - attributeQuote, - attributeValue, - - pseudoMarker, - pseudoClass, - pseudoQuote, - pseudoClassQuotedValue, - pseudoClassValue - ){ - if (separator || separatorIndex === -1){ - parsed.expressions[++separatorIndex] = []; - combinatorIndex = -1; - if (separator) return ''; - } - - if (combinator || combinatorChildren || combinatorIndex === -1){ - combinator = combinator || ' '; - var currentSeparator = parsed.expressions[separatorIndex]; - if (reversed && currentSeparator[combinatorIndex]) - currentSeparator[combinatorIndex].reverseCombinator = reverseCombinator(combinator); - currentSeparator[++combinatorIndex] = {combinator: combinator, tag: '*'}; - } - - var currentParsed = parsed.expressions[separatorIndex][combinatorIndex]; - - if (tagName){ - currentParsed.tag = tagName.replace(reUnescape, ''); - - } else if (id){ - currentParsed.id = id.replace(reUnescape, ''); - - } else if (className){ - className = className.replace(reUnescape, ''); - - if (!currentParsed.classList) currentParsed.classList = []; - if (!currentParsed.classes) currentParsed.classes = []; - currentParsed.classList.push(className); - currentParsed.classes.push({ - value: className, - regexp: new RegExp('(^|\\s)' + escapeRegExp(className) + '(\\s|$)') - }); - - } else if (pseudoClass){ - pseudoClassValue = pseudoClassValue || pseudoClassQuotedValue; - pseudoClassValue = pseudoClassValue ? pseudoClassValue.replace(reUnescape, '') : null; - - if (!currentParsed.pseudos) currentParsed.pseudos = []; - currentParsed.pseudos.push({ - key: pseudoClass.replace(reUnescape, ''), - value: pseudoClassValue, - type: pseudoMarker.length == 1 ? 'class' : 'element' - }); - - } else if (attributeKey){ - attributeKey = attributeKey.replace(reUnescape, ''); - attributeValue = (attributeValue || '').replace(reUnescape, ''); - - var test, regexp; - - switch (attributeOperator){ - 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.indexOf(attributeValue) > -1; - }; break; - case '!=' : test = function(value){ - return attributeValue != value; - }; break; - default : test = function(value){ - return !!value; - }; - } - - if (attributeValue == '' && (/^[*$^]=$/).test(attributeOperator)) test = function(){ - return false; - }; - - if (!test) test = function(value){ - return value && regexp.test(value); - }; - - if (!currentParsed.attributes) currentParsed.attributes = []; - currentParsed.attributes.push({ - key: attributeKey, - operator: attributeOperator, - value: attributeValue, - test: test - }); - - } - - return ''; - }; - - axe.utils.cssParser = parse; -})(axe); diff --git a/test/core/public/run-rules.js b/test/core/public/run-rules.js index bd147aa5d8..c4175a554f 100644 --- a/test/core/public/run-rules.js +++ b/test/core/public/run-rules.js @@ -127,7 +127,7 @@ describe('runRules', function () { any: ['has-target'] }, { id: 'first-div', - selector: 'div:not([id=fixture])', + selector: 'div:not(#fixture)', any: ['first-div'] }], checks: [{ diff --git a/test/core/utils/qsa.js b/test/core/utils/qsa.js index 9d0338a604..0ef699a605 100644 --- a/test/core/utils/qsa.js +++ b/test/core/utils/qsa.js @@ -133,6 +133,42 @@ describe('axe.utils.querySelectorAll', function () { var result = axe.utils.querySelectorAll(dom, '[data-a11yhero="faulkner"] li'); assert.equal(result.length, 2); }); + it('should find nodes using :not selector with class', function () { + var result = axe.utils.querySelectorAll(dom, 'div:not(.first)'); + assert.equal(result.length, 2); + }); + it('should find nodes using :not selector with matching id', function () { + var result = axe.utils.querySelectorAll(dom, 'div:not(#one)'); + assert.equal(result.length, 2); + }); + it('should find nodes using :not selector with matching attribute selector', function () { + var result = axe.utils.querySelectorAll(dom, 'div:not([data-a11yhero])'); + assert.equal(result.length, 2); + }); + it('should find nodes using :not selector with matching attribute selector with value', function () { + var result = axe.utils.querySelectorAll(dom, 'div:not([data-a11yhero=faulkner])'); + assert.equal(result.length, 2); + }); + it('should find nodes using :not selector with bogus attribute selector with value', function () { + var result = axe.utils.querySelectorAll(dom, 'div:not([data-a11yhero=wilco])'); + assert.equal(result.length, 3); + }); + it('should find nodes using :not selector with bogus id', function () { + var result = axe.utils.querySelectorAll(dom, 'div:not(#thangy)'); + assert.equal(result.length, 3); + }); + it('should find nodes hierarchically using :not selector', function () { + var result = axe.utils.querySelectorAll(dom, 'div:not(.first) li'); + assert.equal(result.length, 2); + }); + it('should find same nodes hierarchically using more :not selector', function () { + var result = axe.utils.querySelectorAll(dom, 'div:not(.first) li:not(.breaking)'); + assert.equal(result.length, 2); + }); + it('should NOT find nodes hierarchically using :not selector', function () { + var result = axe.utils.querySelectorAll(dom, 'div:not(.second) li:not(.breaking)'); + assert.equal(result.length, 0); + }); it('should put it all together', function () { var result = axe.utils.querySelectorAll(dom, '.first[data-a11yhero="faulkner"] > ul li.breaking'); From 16bbd0662a1bfa0c0ba1289cecf16d9ba7db9321 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Tue, 28 Mar 2017 08:16:10 -0400 Subject: [PATCH 009/142] update the acknowledgements --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b2264214e7..408da79f3c 100644 --- a/README.md +++ b/README.md @@ -102,4 +102,6 @@ Read the [documentation on contributing](CONTRIBUTING.md) ## Acknowledgements -Thanks to the [Slick Parser](https://github.com/mootools/slick/blob/master/Source/Slick.Parser.js) implementers for their contribution, we have used it in the shadowDOM support code. +Thanks to Dulin Marat for his [css-selector-parser](https://www.npmjs.com/package/css-selector-parser) implementation which is included for shadow DOM support. + +Thanks to the [Slick Parser](https://github.com/mootools/slick/blob/master/Source/Slick.Parser.js) implementers for their contribution, we have used some of their algorithms in our shadow DOM support code. From 5199775e927a44468b9cbe2f82f35bb172836b8b Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Tue, 28 Mar 2017 08:43:54 -0400 Subject: [PATCH 010/142] update the API documentation to reflect the change in the target for a node --- doc/API.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/doc/API.md b/doc/API.md index 4bee0fc006..34313d9697 100644 --- a/doc/API.md +++ b/doc/API.md @@ -415,7 +415,15 @@ This will either be null or an object which is an instance of Error. If you are #### Results Object -The callback function passed in as the third parameter of `axe.a11yCheck` runs on the results object. This object has two components – a passes array and a violations array. The passes array keeps track of all the passed tests, along with detailed information on each one. This leads to more efficient testing, especially when used in conjunction with manual testing, as the user can easily find out what tests have already been passed. Similarly, the violations array keeps track of all the failed tests, along with detailed information on each one. +The callback function passed in as the third parameter of `axe.a11yCheck` runs on the results object. This object has four components – a `passes` array, a `violations` array, an `incomplete` array and an `inapplicable` array. + +The `passes` array keeps track of all the passed tests, along with detailed information on each one. This leads to more efficient testing, especially when used in conjunction with manual testing, as the user can easily find out what tests have already been passed. + +Similarly, the `violations` array keeps track of all the failed tests, along with detailed information on each one. + +The `incomplete` array (also referred to as the "review items") indicates which nodes could neither be determined to definitively pass or definitively fail. They are separated out in order that a user interface can display these to the user for manual review (hence the term "review items"). + +The `inapplicable` array lists all the rules for which no matching elements were found on the page. ###### `url` @@ -444,7 +452,7 @@ Each object returned in these arrays have the following properties: * `nodes` - Array of all elements the Rule tested * `html` - Snippet of HTML of the Element * `impact` - How serious the violation is. Can be one of "minor", "moderate", "serious", or "critical" if the test failed or `null` if the check passed - * `target` - Array of selectors that has each element correspond to one level of iframe or frame. If there is one iframe or frame, there should be two entries in `target`. If there are three iframe levels, there should be four entries in `target`. + * `target` - Array of either strings or Arrays of strings. If the item in the array is a string, then it is a CSS selector. If there are multiple items in the array each item corresponds to one level of iframe or frame. If there is one iframe or frame, there should be two entries in `target`. If there are three iframe levels, there should be four entries in `target`. If the item in the Array is an Array of strings, then it points to an element in a shadow DOM and each item (except the n-1th) in this array is a selector to a DOM element with a shadow DOM. The last element in the array points to the final shadow DOM node. * `any` - Array of checks that were made where at least one must have passed. Each entry in the array contains: * `id` - Unique identifier for this check. Check ids may be the same as Rule ids * `impact` - How serious this particular check is. Can be one of "minor", "moderate", "serious", or "critical". Each check that is part of a rule can have different impacts. The highest impact of all the checks that fail is reported for the rule @@ -527,6 +535,23 @@ axe.run(document, { console.log(results); }); ``` + +#### Example 4 + +This example shows a result object that points to a shadow DOM element. + +* `violations[0]` + * `help` - `"Elements must have sufficient color contrast"` + * `helpUrl` - `"https://dequeuniversity.com/rules/axe/2.1/color-contrast?application=axeAPI"` + * `id` - `"color-contrast"` + * `nodes` + * `target[0][0]` - `"header > aria-menu"` + * `target[0][1]` - `"li.expanded"` + +* `violations[1]` ... + +As you can see the `target` array contains one item that is an array. This array contains two items, the first is a CSS selector string that finds the custom element `` in the `
    `. The second item in this array is the selector within that custom element's shadow DOM to find the `
  • ` element with a class of `expanded`. + ### API Name: axe.registerPlugin Register a plugin with the aXe plugin system. See [implementing a plugin](plugins.md) for more information on the plugin system From 2aac29af075f2007fc6ec4fb118c4a5df0c03e34 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Mon, 1 May 2017 16:46:24 -0500 Subject: [PATCH 011/142] fix: add copyright banner back in to axe.js --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 600a23d31a..42dab3be14 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -193,7 +193,7 @@ module.exports = function (grunt) { bracketize: true, quote_style: 1 }, - preserveComments: 'all' + preserveComments: /^!/ } }, minify: { From 1210321b3fc05f61dc64bc51c99a25371ddfd153 Mon Sep 17 00:00:00 2001 From: Jaime Iniesta Date: Sun, 30 Apr 2017 19:54:47 +0200 Subject: [PATCH 012/142] Add Rocket Validator to doc/projects.md --- doc/projects.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/projects.md b/doc/projects.md index 7f0f88d206..9795021530 100644 --- a/doc/projects.md +++ b/doc/projects.md @@ -24,3 +24,4 @@ Add your project/integration to this file and submit a pull request. 20. [Storybook accessibility addon](https://github.com/jbovenschen/storybook-addon-a11y) 21. [Intern](https://github.com/theintern/intern-a11y) 22. [Protractor-axe-report Plugin](https://github.com/E1Edatatracker/protractor-axe-report-plugin) +23. [Rocket Validator](https://rocketvalidator.com) From cd3b1a5931515f7b3f575d176780ca72ba0295b0 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Mon, 1 May 2017 17:11:56 -0500 Subject: [PATCH 013/142] chore: add worldspace to projects --- doc/projects.md | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/doc/projects.md b/doc/projects.md index 9795021530..4c66a14cc2 100644 --- a/doc/projects.md +++ b/doc/projects.md @@ -2,26 +2,29 @@ Add your project/integration to this file and submit a pull request. +1. [WorldSpace Attest](https://www.deque.com/products/worldspace-attest/) +1. [WorldSpace Assure](https://www.deque.com/products/worldspace-assure/) +1. [WorldSpace Comply](https://www.deque.com/products/worldspace-comply/) 1. [aXe Chrome plugin](https://chrome.google.com/webstore/detail/axe/lhdoppojpmngadmnindnejefpokejbdd) -2. [axe-webdriverjs](https://www.npmjs.com/package/axe-webdriverjs) -3. [ember-a11y-testing](https://www.npmjs.com/package/ember-a11y-testing) -4. [axe-firefox-devtools](https://github.com/dequelabs/axe-firefox-devtools) and on the [Firefox extension page](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/) -5. [axe-selenium-java](https://github.com/dequelabs/axe-selenium-java) -6. [a11yChromePlugin - not the official Chrome plugin source code](https://github.com/ptrstpp950/a11yChromePlugin) -7. [grunt-axe-webdriver](https://www.npmjs.com/package/grunt-axe-webdriver) -8. [R-Spec and Cucumber](https://github.com/dequelabs/axe-matchers) -9. [aXe audit runner for CrawlKit](https://github.com/crawlkit/runner-axe) -10. [Web Accessibility Checker for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/3aabefab-1681-4fea-8f95-6a62e2f0f1ec) -11. [ReactJS Accessibility Checker](https://github.com/dylanb/react-axe) (react-axe) -12. [Vorlon.js Remote Debugger](https://github.com/MicrosoftDX/Vorlonjs) -13. [Selenium IDE aXe Extension](https://github.com/bkardell/selenium-ide-axe) -14. [gulp-axe-webdriver](https://github.com/felixzapata/gulp-axe-webdriver) -15. [AccessLint](https://accesslint.com/) -16. [Lighthouse](https://github.com/GoogleChrome/lighthouse) -17. [Axegrinder](https://github.com/claflamme/axegrinder) -18. [Ghost-Axe](https://www.npmjs.com/package/ghost-axe) -19. [Protractor accessibility plugin](https://github.com/angular/protractor-accessibility-plugin) -20. [Storybook accessibility addon](https://github.com/jbovenschen/storybook-addon-a11y) -21. [Intern](https://github.com/theintern/intern-a11y) -22. [Protractor-axe-report Plugin](https://github.com/E1Edatatracker/protractor-axe-report-plugin) -23. [Rocket Validator](https://rocketvalidator.com) +1. [axe-webdriverjs](https://www.npmjs.com/package/axe-webdriverjs) +1. [ember-a11y-testing](https://www.npmjs.com/package/ember-a11y-testing) +1. [axe-firefox-devtools](https://github.com/dequelabs/axe-firefox-devtools) and on the [Firefox extension page](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/) +1. [axe-selenium-java](https://github.com/dequelabs/axe-selenium-java) +1. [a11yChromePlugin - not the official Chrome plugin source code](https://github.com/ptrstpp950/a11yChromePlugin) +1. [grunt-axe-webdriver](https://www.npmjs.com/package/grunt-axe-webdriver) +1. [R-Spec and Cucumber](https://github.com/dequelabs/axe-matchers) +1. [aXe audit runner for CrawlKit](https://github.com/crawlkit/runner-axe) +1. [Web Accessibility Checker for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/3aabefab-1681-4fea-8f95-6a62e2f0f1ec) +1. [ReactJS Accessibility Checker](https://github.com/dylanb/react-axe) (react-axe) +1. [Vorlon.js Remote Debugger](https://github.com/MicrosoftDX/Vorlonjs) +1. [Selenium IDE aXe Extension](https://github.com/bkardell/selenium-ide-axe) +1. [gulp-axe-webdriver](https://github.com/felixzapata/gulp-axe-webdriver) +1. [AccessLint](https://accesslint.com/) +1. [Lighthouse](https://github.com/GoogleChrome/lighthouse) +1. [Axegrinder](https://github.com/claflamme/axegrinder) +1. [Ghost-Axe](https://www.npmjs.com/package/ghost-axe) +1. [Protractor accessibility plugin](https://github.com/angular/protractor-accessibility-plugin) +1. [Storybook accessibility addon](https://github.com/jbovenschen/storybook-addon-a11y) +1. [Intern](https://github.com/theintern/intern-a11y) +1. [Protractor-axe-report Plugin](https://github.com/E1Edatatracker/protractor-axe-report-plugin) +1. [Rocket Validator](https://rocketvalidator.com) From 07ea17a868604359839dbf6c2b2176f1bf32ea4e Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sat, 20 May 2017 16:57:42 -0400 Subject: [PATCH 014/142] Add support for V1 fallback content --- lib/core/utils/composed-tree.js | 15 +++++++++++++++ test/core/utils/composed-tree.js | 10 +++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/core/utils/composed-tree.js b/lib/core/utils/composed-tree.js index 954f62227a..e0a8a141cd 100644 --- a/lib/core/utils/composed-tree.js +++ b/lib/core/utils/composed-tree.js @@ -43,6 +43,17 @@ function virtualDOMfromNode (node, shadowId) { }; } +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 of course the shadow DOM nodes @@ -82,6 +93,10 @@ axe.utils.getComposedTree = function (node, shadowId) { return realArray.reduce(reduceShadowDOM, []); } else if (nodeName === 'slot') { realArray = Array.from(node.assignedNodes()); + if (!realArray.length) { + // fallback content + realArray = getSlotChildren(node); + } return realArray.reduce(reduceShadowDOM, []); } else { if (node.nodeType === 1) { diff --git a/test/core/utils/composed-tree.js b/test/core/utils/composed-tree.js index 5772daf92c..b4433e3485 100644 --- a/test/core/utils/composed-tree.js +++ b/test/core/utils/composed-tree.js @@ -111,7 +111,7 @@ if (document.body && typeof document.body.attachShadow === 'function') { var group = document.createElement('div'); group.className = className; // Empty string in slot name attribute or absence thereof work the same, so no need for special handling. - group.innerHTML = '
    '; + group.innerHTML = '
      fallback content
    • one
    '; return group; } @@ -127,6 +127,7 @@ if (document.body && typeof document.body.attachShadow === 'function') { str += '
  • 1
  • ' + '
  • 2
  • 3
  • ' + '
  • 4
  • 5
  • 6
  • '; + str += '
    '; fixture.innerHTML = str; fixture.querySelectorAll('.stories').forEach(makeShadowTree); @@ -139,6 +140,13 @@ if (document.body && typeof document.body.attachShadow === 'function') { }); it('getComposedTree\'s virtual DOM should represent the composed tree', composedTreeAssertions); it('getComposedTree\'s virtual DOM should give an ID to the shadow DOM', shadowIdAssertions); + it('getComposedTree\'s virtual DOM should have the fallback content', function () { + var virtualDOM = axe.utils.getComposedTree(fixture); + assert.isTrue(virtualDOM[0].children[7].children[0].children.length === 2); + assert.isTrue(virtualDOM[0].children[7].children[0].children[0].actualNode.nodeType === 3); + assert.isTrue(virtualDOM[0].children[7].children[0].children[0].actualNode.textContent === 'fallback content'); + assert.isTrue(virtualDOM[0].children[7].children[0].children[1].actualNode.nodeName === 'LI'); + }); }); } From 48b56685ee7f2b10d799d6bcb077c1b34e174e7f Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sat, 20 May 2017 17:35:56 -0400 Subject: [PATCH 015/142] fix some tests --- lib/commons/dom/is-offscreen.js | 4 +++- test/core/utils/send-command-to-frame.js | 22 ++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/commons/dom/is-offscreen.js b/lib/commons/dom/is-offscreen.js index a27cb9688c..2d4ef7c3be 100644 --- a/lib/commons/dom/is-offscreen.js +++ b/lib/commons/dom/is-offscreen.js @@ -18,12 +18,14 @@ dom.isOffscreen = function (element) { var leftBoundary, docElement = document.documentElement, + styl = window.getComputedStyle(element), dir = window.getComputedStyle(document.body || docElement) .getPropertyValue('direction'), coords = dom.getElementCoordinates(element); // bottom edge beyond - if (coords.bottom < 0 && noParentScrolled(element, coords.bottom)) { + if (coords.bottom < 0 && (noParentScrolled(element, coords.bottom) || + styl.position === 'absolute')) { return true; } diff --git a/test/core/utils/send-command-to-frame.js b/test/core/utils/send-command-to-frame.js index e606bba534..b2f34e1d42 100644 --- a/test/core/utils/send-command-to-frame.js +++ b/test/core/utils/send-command-to-frame.js @@ -60,16 +60,18 @@ describe('axe.utils.sendCommandToFrame', function () { var called = 0; var frame = document.createElement('iframe'); frame.addEventListener('load', function () { - axe.utils.sendCommandToFrame(frame, { - number: number, - keepalive: true - }, function () { - called += 1; - if (called === number) { - assert.isTrue(true); - done(); - } - }, assertNotCalled); + setTimeout(function () { + axe.utils.sendCommandToFrame(frame, { + number: number, + keepalive: true + }, function () { + called += 1; + if (called === number) { + assert.isTrue(true); + done(); + } + }, assertNotCalled); + }, 500); }); frame.id = 'level0'; From 264651f368a56c798e84a1f7c4a0527e7e770962 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Wed, 24 May 2017 06:46:25 -0400 Subject: [PATCH 016/142] change the terminology to match the shadow DOM spec as integrated into the HTML spec --- lib/core/base/context.js | 6 +-- .../{composed-tree.js => flattened-tree.js} | 16 +++---- test/core/base/audit.js | 8 ++-- test/core/base/check.js | 2 +- test/core/base/context.js | 6 +-- test/core/base/rule.js | 46 +++++++++---------- test/core/utils/contains.js | 4 +- .../{composed-tree.js => flattened-tree.js} | 32 ++++++------- test/core/utils/select.js | 34 +++++++------- 9 files changed, 77 insertions(+), 77 deletions(-) rename lib/core/utils/{composed-tree.js => flattened-tree.js} (87%) rename test/core/utils/{composed-tree.js => flattened-tree.js} (81%) diff --git a/lib/core/base/context.js b/lib/core/base/context.js index cbb0c8aa1f..3224c88c45 100644 --- a/lib/core/base/context.js +++ b/lib/core/base/context.js @@ -129,7 +129,7 @@ function parseSelectorArray(context, type) { nodeList = Array.from(document.querySelectorAll(item)); //jshint loopfunc:true result = result.concat(nodeList.map((node) => { - return axe.utils.getComposedTree(node)[0]; + return axe.utils.getFlattenedTree(node)[0]; })); break; } else if (item && item.length && !(item instanceof Node)) { @@ -140,12 +140,12 @@ function parseSelectorArray(context, type) { nodeList = Array.from(document.querySelectorAll(item[0])); //jshint loopfunc:true result = result.concat(nodeList.map((node) => { - return axe.utils.getComposedTree(node)[0]; + return axe.utils.getFlattenedTree(node)[0]; })); } } else if (item instanceof Node) { - result.push(axe.utils.getComposedTree(item)[0]); + result.push(axe.utils.getFlattenedTree(item)[0]); } } diff --git a/lib/core/utils/composed-tree.js b/lib/core/utils/flattened-tree.js similarity index 87% rename from lib/core/utils/composed-tree.js rename to lib/core/utils/flattened-tree.js index e0a8a141cd..af6be946a5 100644 --- a/lib/core/utils/composed-tree.js +++ b/lib/core/utils/flattened-tree.js @@ -1,14 +1,14 @@ /* global console */ var axe = axe || { utils: {} }; /** - * NOTE: level only increases on "real" nodes because others do not exist in the composed tree + * NOTE: level only increases on "real" nodes because others do not exist in the flattened tree */ -axe.utils.printComposedTree = function (node, level) { +axe.utils.printFlattenedTree = function (node, level) { var indent = ' '.repeat(level) + '\u2514> '; var nodeName; if (node.shadowRoot) { node.shadowRoot.childNodes.forEach(function (child) { - axe.utils.printComposedTree(child, level); + axe.utils.printFlattenedTree(child, level); }); } else { nodeName = node.nodeName.toLowerCase(); @@ -17,17 +17,17 @@ axe.utils.printComposedTree = function (node, level) { } if (nodeName === 'content') { node.getDistributedNodes().forEach(function (child) { - axe.utils.printComposedTree(child, level); + axe.utils.printFlattenedTree(child, level); }); } else if (nodeName === 'slot') { node.assignedNodes().forEach(function (child) { - axe.utils.printComposedTree(child, level); + axe.utils.printFlattenedTree(child, level); }); } else { if (node.nodeType === 1) { console.log(indent, node); node.childNodes.forEach(function (child) { - axe.utils.printComposedTree(child, level + 1); + axe.utils.printFlattenedTree(child, level + 1); }); } } @@ -64,12 +64,12 @@ function getSlotChildren(node) { * ancestor of the node */ -axe.utils.getComposedTree = function (node, shadowId) { +axe.utils.getFlattenedTree = function (node, shadowId) { // using a closure here and therefore cannot easily refactor toreduce the statements //jshint maxstatements: false var retVal, realArray, nodeName; function reduceShadowDOM (res, child) { - var replacements = axe.utils.getComposedTree(child, shadowId); + var replacements = axe.utils.getFlattenedTree(child, shadowId); if (replacements) { res = res.concat(replacements); } diff --git a/test/core/base/audit.js b/test/core/base/audit.js index 08ca791c36..30e42e8384 100644 --- a/test/core/base/audit.js +++ b/test/core/base/audit.js @@ -368,7 +368,7 @@ describe('Audit', function () { '' + 'FAIL ME'; - a.run({ include: [axe.utils.getComposedTree(fixture)[0]] }, {}, function (results) { + a.run({ include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, function (results) { var expected = [{ id: 'positive1', result: 'inapplicable', @@ -510,7 +510,7 @@ describe('Audit', function () { } }); - a.run({ include: [axe.utils.getComposedTree(fixture)[0]] }, { + a.run({ include: [axe.utils.getFlattenedTree(fixture)[0]] }, { runOnly: { 'type': 'rule', 'values': ['throw1'] @@ -537,7 +537,7 @@ describe('Audit', function () { throw new Error('Launch the super sheep!'); } }); - a.run({ include: [axe.utils.getComposedTree(fixture)[0]] }, { + a.run({ include: [axe.utils.getFlattenedTree(fixture)[0]] }, { runOnly: { 'type': 'rule', 'values': ['throw1', 'positive1'] @@ -575,7 +575,7 @@ describe('Audit', function () { throw new Error('Launch the super sheep!'); } }); - a.run({ include: [axe.utils.getComposedTree(fixture)[0]] }, { + a.run({ include: [axe.utils.getFlattenedTree(fixture)[0]] }, { debug: true, runOnly: { 'type': 'rule', diff --git a/test/core/base/check.js b/test/core/base/check.js index fde3d4d8ab..484eae5ced 100644 --- a/test/core/base/check.js +++ b/test/core/base/check.js @@ -128,7 +128,7 @@ describe('Check', function () { assert.equal(node, fixture); done(); } - }).run(axe.utils.getComposedTree(fixture)[0], {}, noop); + }).run(axe.utils.getFlattenedTree(fixture)[0], {}, noop); }); diff --git a/test/core/base/context.js b/test/core/base/context.js index a83914d41e..86f253616d 100644 --- a/test/core/base/context.js +++ b/test/core/base/context.js @@ -335,8 +335,8 @@ describe('Context', function() { include: ['#fixture'], exclude: ['#mocha'] }), { - include: [axe.utils.getComposedTree(document.getElementById('fixture'))[0]], - exclude: [axe.utils.getComposedTree(document.getElementById('mocha'))[0]], + include: [axe.utils.getFlattenedTree(document.getElementById('fixture'))[0]], + exclude: [axe.utils.getFlattenedTree(document.getElementById('mocha'))[0]], initiator: true, page: false, frames: [] @@ -349,7 +349,7 @@ describe('Context', function() { include: ['#fixture', '#monkeys'], exclude: ['#bananas'] }), { - include: [axe.utils.getComposedTree(document.getElementById('fixture'))[0]], + include: [axe.utils.getFlattenedTree(document.getElementById('fixture'))[0]], exclude: [], initiator: true, page: false, diff --git a/test/core/base/rule.js b/test/core/base/rule.js index 56d2caafc7..a371593ea9 100644 --- a/test/core/base/rule.js +++ b/test/core/base/rule.js @@ -31,7 +31,7 @@ describe('Rule', function() { selector: '#monkeys' }), nodes = rule.gather({ - include: [axe.utils.getComposedTree(fixture)[0]], + include: [axe.utils.getFlattenedTree(fixture)[0]], exclude: [], frames: [] }); @@ -41,7 +41,7 @@ describe('Rule', function() { node.id = 'bananas'; nodes = rule.gather({ - include: [axe.utils.getComposedTree(fixture)[0]], + include: [axe.utils.getFlattenedTree(fixture)[0]], exclude: [], frames: [] }); @@ -54,7 +54,7 @@ describe('Rule', function() { selector: 'div' }), result = rule.gather({ - include: [axe.utils.getComposedTree(fixture)[0]], + include: [axe.utils.getFlattenedTree(fixture)[0]], exclude: [], frames: [] }); @@ -70,7 +70,7 @@ describe('Rule', function() { selector: 'div' }), nodes = rule.gather({ - include: [axe.utils.getComposedTree(document.getElementById('fixture').firstChild)[0]] + include: [axe.utils.getFlattenedTree(document.getElementById('fixture').firstChild)[0]] }); assert.deepEqual(nodes.map(function (n) {return n.actualNode;}), [node]); @@ -90,7 +90,7 @@ describe('Rule', function() { var rule = new Rule({}), result = rule.gather({ - include: [axe.utils.getComposedTree(document.getElementById('fixture'))[0]] + include: [axe.utils.getFlattenedTree(document.getElementById('fixture'))[0]] }); assert.lengthOf(result, 3); @@ -102,7 +102,7 @@ describe('Rule', function() { var rule = new Rule({}), result = rule.gather({ - include: [axe.utils.getComposedTree(document.getElementById('fixture').firstChild)[0]] + include: [axe.utils.getFlattenedTree(document.getElementById('fixture').firstChild)[0]] }); assert.lengthOf(result, 0); @@ -114,7 +114,7 @@ describe('Rule', function() { excludeHidden: false }), result = rule.gather({ - include: [axe.utils.getComposedTree(document.getElementById('fixture').firstChild)[0]] + include: [axe.utils.getFlattenedTree(document.getElementById('fixture').firstChild)[0]] }); assert.deepEqual(result.map(function (n) { return n.actualNode; }), [fixture.firstChild]); @@ -138,7 +138,7 @@ describe('Rule', function() { }); rule.run({ - include: [axe.utils.getComposedTree(div)[0]] + include: [axe.utils.getFlattenedTree(div)[0]] }, {}, function() { assert.isTrue(success); done(); @@ -158,7 +158,7 @@ describe('Rule', function() { }); rule.run({ - include: [axe.utils.getComposedTree(div)[0]] + include: [axe.utils.getFlattenedTree(div)[0]] }, {}, isNotCalled, function() { assert.isFalse(success); done(); @@ -182,7 +182,7 @@ describe('Rule', function() { }); rule.run({ - include: [axe.utils.getComposedTree(fixture)[0]] + include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, function() { assert.isTrue(success); done(); @@ -207,7 +207,7 @@ describe('Rule', function() { }); rule.run({ - include: [axe.utils.getComposedTree(fixture)[0]] + include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, function() { assert.isTrue(success); done(); @@ -232,7 +232,7 @@ describe('Rule', function() { }, isNotCalled); rule.run({ - include: [axe.utils.getComposedTree(fixture)[0]] + include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, function() { assert.isTrue(success); done(); @@ -265,7 +265,7 @@ describe('Rule', function() { } }); rule.run({ - include: [axe.utils.getComposedTree(document)[0]] + include: [axe.utils.getFlattenedTree(document)[0]] }, options, function() { done(); }, isNotCalled); @@ -310,7 +310,7 @@ describe('Rule', function() { } }); rule.run({ - include: [axe.utils.getComposedTree(document)[0]] + include: [axe.utils.getFlattenedTree(document)[0]] }, options, function() { done(); }, isNotCalled); @@ -329,7 +329,7 @@ describe('Rule', function() { } }); rule.run({ - include: [axe.utils.getComposedTree(document)[0]] + include: [axe.utils.getFlattenedTree(document)[0]] }, {}, function(r) { assert.lengthOf(r.nodes, 0); }, isNotCalled); @@ -370,7 +370,7 @@ describe('Rule', function() { } }); rule.run({ - include: [axe.utils.getComposedTree(fixture)[0]] + include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, function() { assert.isTrue(isDqElementCalled); done(); @@ -393,7 +393,7 @@ describe('Rule', function() { } }); rule.run({ - include: [axe.utils.getComposedTree(fixture)[0]] + include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, function() { assert.isFalse(isDqElementCalled); done(); @@ -415,7 +415,7 @@ describe('Rule', function() { } }); rule.run({ - include: [axe.utils.getComposedTree(fixture)[0]] + include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, function() { assert.isTrue(isDqElementCalled); done(); @@ -435,7 +435,7 @@ describe('Rule', function() { } }); rule.run({ - include: [axe.utils.getComposedTree(fixture)[0]] + include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, function() { assert.isFalse(isDqElementCalled); done(); @@ -458,7 +458,7 @@ describe('Rule', function() { }); rule.run({ - include: [axe.utils.getComposedTree(fixture)[0]] + include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, noop, function(err) { assert.equal(err.message, 'Holy hand grenade'); done(); @@ -480,7 +480,7 @@ describe('Rule', function() { }); rule.run({ - include: [axe.utils.getComposedTree(fixture)[0]] + include: [axe.utils.getFlattenedTree(fixture)[0]] }, {}, noop, function(err) { assert.equal(err.message, 'your reality'); done(); @@ -504,7 +504,7 @@ describe('Rule', function() { }] }); rule.run({ - include: axe.utils.getComposedTree(document)[0] + include: axe.utils.getFlattenedTree(document)[0] }, {}, noop, isNotCalled); assert.isTrue(success); @@ -521,7 +521,7 @@ describe('Rule', function() { }] }); rule.run({ - include: axe.utils.getComposedTree(document)[0] + include: axe.utils.getFlattenedTree(document)[0] }, {}, function() { success = true; }, isNotCalled); diff --git a/test/core/utils/contains.js b/test/core/utils/contains.js index c0e2bf8d67..7cbbefd754 100644 --- a/test/core/utils/contains.js +++ b/test/core/utils/contains.js @@ -59,8 +59,8 @@ describe('axe.utils.contains', function () { it('should work', function () { fixture.innerHTML = '
    '; - var inner = axe.utils.getComposedTree(document.getElementById('inner'))[0]; - var outer = axe.utils.getComposedTree(document.getElementById('outer'))[0]; + var inner = axe.utils.getFlattenedTree(document.getElementById('inner'))[0]; + var outer = axe.utils.getFlattenedTree(document.getElementById('outer'))[0]; assert.isTrue(axe.utils.contains(outer, inner)); assert.isFalse(axe.utils.contains(inner, outer)); diff --git a/test/core/utils/composed-tree.js b/test/core/utils/flattened-tree.js similarity index 81% rename from test/core/utils/composed-tree.js rename to test/core/utils/flattened-tree.js index b4433e3485..f4b5e06dbe 100644 --- a/test/core/utils/composed-tree.js +++ b/test/core/utils/flattened-tree.js @@ -9,10 +9,10 @@ function createStyle () { return style; } -function composedTreeAssertions () { +function flattenedTreeAssertions () { 'use strict'; - var virtualDOM = axe.utils.getComposedTree(fixture.firstChild); + var virtualDOM = axe.utils.getFlattenedTree(fixture.firstChild); assert.equal(virtualDOM.length, 3); assert.equal(virtualDOM[0].actualNode.nodeName, 'STYLE'); @@ -42,7 +42,7 @@ function composedTreeAssertions () { function shadowIdAssertions () { 'use strict'; - var virtualDOM = axe.utils.getComposedTree(fixture); + var virtualDOM = axe.utils.getFlattenedTree(fixture); assert.isUndefined(virtualDOM[0].shadowId); assert.isDefined(virtualDOM[0].children[0].shadowId); assert.isDefined(virtualDOM[0].children[1].shadowId); @@ -60,7 +60,7 @@ function shadowIdAssertions () { } if (document.body && typeof document.body.createShadowRoot === 'function') { - describe('composed-tree shadow DOM v0', function () { + describe('flattened-tree shadow DOM v0', function () { 'use strict'; afterEach(function () { fixture.innerHTML = ''; @@ -92,16 +92,16 @@ if (document.body && typeof document.body.createShadowRoot === 'function') { it('it should support shadow DOM v0', function () { assert.isDefined(fixture.firstChild.shadowRoot); }); - it('getComposedTree should return an array of stuff', function () { - assert.isTrue(Array.isArray(axe.utils.getComposedTree(fixture.firstChild))); + it('getFlattenedTree should return an array of stuff', function () { + assert.isTrue(Array.isArray(axe.utils.getFlattenedTree(fixture.firstChild))); }); - it('getComposedTree\'s virtual DOM should represent the composed tree', composedTreeAssertions); - it('getComposedTree\'s virtual DOM should give an ID to the shadow DOM', shadowIdAssertions); + it('getFlattenedTree\'s virtual DOM should represent the flattened tree', flattenedTreeAssertions); + it('getFlattenedTree\'s virtual DOM should give an ID to the shadow DOM', shadowIdAssertions); }); } if (document.body && typeof document.body.attachShadow === 'function') { - describe('composed-tree shadow DOM v1', function () { + describe('flattened-tree shadow DOM v1', function () { 'use strict'; afterEach(function () { fixture.innerHTML = ''; @@ -135,13 +135,13 @@ if (document.body && typeof document.body.attachShadow === 'function') { it('should support shadow DOM v1', function () { assert.isDefined(fixture.firstChild.shadowRoot); }); - it('getComposedTree should return an array of stuff', function () { - assert.isTrue(Array.isArray(axe.utils.getComposedTree(fixture.firstChild))); + it('getFlattenedTree should return an array of stuff', function () { + assert.isTrue(Array.isArray(axe.utils.getFlattenedTree(fixture.firstChild))); }); - it('getComposedTree\'s virtual DOM should represent the composed tree', composedTreeAssertions); - it('getComposedTree\'s virtual DOM should give an ID to the shadow DOM', shadowIdAssertions); - it('getComposedTree\'s virtual DOM should have the fallback content', function () { - var virtualDOM = axe.utils.getComposedTree(fixture); + it('getFlattenedTree\'s virtual DOM should represent the flattened tree', flattenedTreeAssertions); + it('getFlattenedTree\'s virtual DOM should give an ID to the shadow DOM', shadowIdAssertions); + it('getFlattenedTree\'s virtual DOM should have the fallback content', function () { + var virtualDOM = axe.utils.getFlattenedTree(fixture); assert.isTrue(virtualDOM[0].children[7].children[0].children.length === 2); assert.isTrue(virtualDOM[0].children[7].children[0].children[0].actualNode.nodeType === 3); assert.isTrue(virtualDOM[0].children[7].children[0].children[0].actualNode.textContent === 'fallback content'); @@ -152,7 +152,7 @@ if (document.body && typeof document.body.attachShadow === 'function') { if (document.body && typeof document.body.attachShadow === 'undefined' && typeof document.body.createShadowRoot === 'undefined') { - describe('composed-tree', function () { + describe('flattened-tree', function () { 'use strict'; it('SHADOW DOM TESTS DEFERRED, NO SUPPORT'); }); diff --git a/test/core/utils/select.js b/test/core/utils/select.js index e7f8f8fb93..9ca3fa5d11 100644 --- a/test/core/utils/select.js +++ b/test/core/utils/select.js @@ -28,7 +28,7 @@ describe('axe.utils.select', function () { div.id = 'monkeys'; fixture.appendChild(div); - var result = axe.utils.select('#monkeys', { include: [axe.utils.getComposedTree(document)[0]] }); + var result = axe.utils.select('#monkeys', { include: [axe.utils.getFlattenedTree(document)[0]] }); assert.equal(result[0].actualNode, div); @@ -41,7 +41,7 @@ describe('axe.utils.select', function () { fixture.innerHTML = '
    '; var result = axe.utils.select('.bananas', { - include: [axe.utils.getComposedTree($id('monkeys'))[0]] + include: [axe.utils.getFlattenedTree($id('monkeys'))[0]] }); assert.deepEqual([result[0].actualNode], [$id('bananas')]); @@ -52,8 +52,8 @@ describe('axe.utils.select', function () { fixture.innerHTML = '
    '; var result = axe.utils.select('.bananas', { - include: [axe.utils.getComposedTree($id('fixture'))[0]], - exclude: [axe.utils.getComposedTree($id('monkeys'))[0]] + include: [axe.utils.getFlattenedTree($id('fixture'))[0]], + exclude: [axe.utils.getFlattenedTree($id('monkeys'))[0]] }); assert.deepEqual(result, []); @@ -73,10 +73,10 @@ describe('axe.utils.select', function () { var result = axe.utils.select('.bananas', { - include: [axe.utils.getComposedTree($id('include1'))[0], - axe.utils.getComposedTree($id('include2'))[0]], - exclude: [axe.utils.getComposedTree($id('exclude1'))[0], - axe.utils.getComposedTree($id('exclude2'))[0]] + include: [axe.utils.getFlattenedTree($id('include1'))[0], + axe.utils.getFlattenedTree($id('include2'))[0]], + exclude: [axe.utils.getFlattenedTree($id('exclude1'))[0], + axe.utils.getFlattenedTree($id('exclude2'))[0]] }); assert.deepEqual(result, []); @@ -98,11 +98,11 @@ describe('axe.utils.select', function () { var result = axe.utils.select('.bananas', { - include: [axe.utils.getComposedTree($id('include3'))[0], - axe.utils.getComposedTree($id('include2'))[0], - axe.utils.getComposedTree($id('include1'))[0]], - exclude: [axe.utils.getComposedTree($id('exclude1'))[0], - axe.utils.getComposedTree($id('exclude2'))[0]] + include: [axe.utils.getFlattenedTree($id('include3'))[0], + axe.utils.getFlattenedTree($id('include2'))[0], + axe.utils.getFlattenedTree($id('include1'))[0]], + exclude: [axe.utils.getFlattenedTree($id('exclude1'))[0], + axe.utils.getFlattenedTree($id('exclude2'))[0]] }); assert.deepEqual([result[0].actualNode], [$id('bananas')]); @@ -115,8 +115,8 @@ describe('axe.utils.select', function () { fixture.innerHTML = '
    '; var result = axe.utils.select('.bananas', { - include: [axe.utils.getComposedTree($id('fixture'))[0], - axe.utils.getComposedTree($id('monkeys'))[0]] + include: [axe.utils.getFlattenedTree($id('fixture'))[0], + axe.utils.getFlattenedTree($id('monkeys'))[0]] }); assert.lengthOf(result, 1); @@ -128,8 +128,8 @@ describe('axe.utils.select', function () { fixture.innerHTML = '
    ' + '
    '; - var result = axe.utils.select('.bananas', { include: [axe.utils.getComposedTree($id('two'))[0], - axe.utils.getComposedTree($id('one'))[0]] }); + var result = axe.utils.select('.bananas', { include: [axe.utils.getFlattenedTree($id('two'))[0], + axe.utils.getFlattenedTree($id('one'))[0]] }); assert.deepEqual(result.map(function (n) { return n.actualNode; }), [$id('target1'), $id('target2')]); From 8713b379163c6a46e83a02addcc17cdfbfaba4f2 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Thu, 25 May 2017 12:38:41 -0400 Subject: [PATCH 017/142] add support for including styled slot elements into the virtual DOM --- lib/core/utils/flattened-tree.js | 73 ++++++++++++++++--------------- test/core/utils/flattened-tree.js | 39 ++++++++++++++++- 2 files changed, 76 insertions(+), 36 deletions(-) diff --git a/lib/core/utils/flattened-tree.js b/lib/core/utils/flattened-tree.js index af6be946a5..d8c1254a81 100644 --- a/lib/core/utils/flattened-tree.js +++ b/lib/core/utils/flattened-tree.js @@ -1,41 +1,29 @@ -/* global console */ var axe = axe || { utils: {} }; + /** - * NOTE: level only increases on "real" nodes because others do not exist in the flattened tree + * 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: + * 1. elements do not have boxes by default (i.e. they do not get rendered and + * their CSS properties are ignored) + * 2. elements can be made to have a box by overriding the display property + * which is 'contents' by default + * 3. Even boxed 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) */ -axe.utils.printFlattenedTree = function (node, level) { - var indent = ' '.repeat(level) + '\u2514> '; - var nodeName; - if (node.shadowRoot) { - node.shadowRoot.childNodes.forEach(function (child) { - axe.utils.printFlattenedTree(child, level); - }); - } else { - nodeName = node.nodeName.toLowerCase(); - if (['style', 'template', 'script'].indexOf(nodeName) !== -1) { - return; - } - if (nodeName === 'content') { - node.getDistributedNodes().forEach(function (child) { - axe.utils.printFlattenedTree(child, level); - }); - } else if (nodeName === 'slot') { - node.assignedNodes().forEach(function (child) { - axe.utils.printFlattenedTree(child, level); - }); - } else { - if (node.nodeType === 1) { - console.log(indent, node); - node.childNodes.forEach(function (child) { - axe.utils.printFlattenedTree(child, level + 1); - }); - } - } - } -}; +/** + * 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 + * @return {Object} - the wrapped node + */ function virtualDOMfromNode (node, shadowId) { - // todo: attributes'n shit (maybe) return { shadowId: shadowId, children: [], @@ -43,6 +31,13 @@ function virtualDOMfromNode (node, shadowId) { }; } +/** + * find all the fallback content for a 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 = []; @@ -63,7 +58,6 @@ function getSlotChildren(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) { // using a closure here and therefore cannot easily refactor toreduce the statements //jshint maxstatements: false @@ -97,7 +91,16 @@ axe.utils.getFlattenedTree = function (node, shadowId) { // fallback content realArray = getSlotChildren(node); } - return realArray.reduce(reduceShadowDOM, []); + var styl = window.getComputedStyle(node); + // check the display property + if (styl.display !== 'contents') { + // has a box + retVal = virtualDOMfromNode(node, shadowId); + retVal.children = realArray.reduce(reduceShadowDOM, []); + return [retVal]; + } else { + return realArray.reduce(reduceShadowDOM, []); + } } else { if (node.nodeType === 1) { retVal = virtualDOMfromNode(node, shadowId); diff --git a/test/core/utils/flattened-tree.js b/test/core/utils/flattened-tree.js index f4b5e06dbe..eb097f0952 100644 --- a/test/core/utils/flattened-tree.js +++ b/test/core/utils/flattened-tree.js @@ -1,10 +1,11 @@ var fixture = document.getElementById('fixture'); -function createStyle () { +function createStyle (box) { 'use strict'; var style = document.createElement('style'); style.textContent = 'div.breaking { color: Red;font-size: 20px; border: 1px dashed Purple; }' + + (box ? 'slot { display: block; }' : '') + 'div.other { padding: 2px 0 0 0; border: 1px solid Cyan; }'; return style; } @@ -148,6 +149,42 @@ if (document.body && typeof document.body.attachShadow === 'function') { assert.isTrue(virtualDOM[0].children[7].children[0].children[1].actualNode.nodeName === 'LI'); }); }); + describe('flattened-tree shadow DOM v1: boxed slots', function () { + 'use strict'; + afterEach(function () { + fixture.innerHTML = ''; + }); + beforeEach(function () { + function createStoryGroup (className, slotName) { + var group = document.createElement('div'); + group.className = className; + // Empty string in slot name attribute or absence thereof work the same, so no need for special handling. + group.innerHTML = '
      fallback content
    • one
    '; + return group; + } + + function makeShadowTree (storyList) { + var root = storyList.attachShadow({mode: 'open'}); + root.appendChild(createStyle(true)); + root.appendChild(createStoryGroup('breaking', 'breaking')); + root.appendChild(createStoryGroup('other', '')); + } + var str = '
  • 1
  • ' + + '
  • 2
  • 3
  • ' + + '
  • 4
  • 5
  • 6
  • '; + str += '
  • 1
  • ' + + '
  • 2
  • 3
  • ' + + '
  • 4
  • 5
  • 6
  • '; + str += '
    '; + fixture.innerHTML = str; + + fixture.querySelectorAll('.stories').forEach(makeShadowTree); + }); + it('getFlattenedTree\'s virtual DOM should have the elements', function () { + var virtualDOM = axe.utils.getFlattenedTree(fixture); + assert.isTrue(virtualDOM[0].children[1].children[0].children[0].actualNode.nodeName === 'SLOT'); + }); + }); } if (document.body && typeof document.body.attachShadow === 'undefined' && From d489c43b59fef0a52b9c9f295c544e9b91124731 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Fri, 9 Jun 2017 17:58:51 -0400 Subject: [PATCH 018/142] disabling slot styling support until it is supported by Chrome --- lib/core/utils/flattened-tree.js | 4 +++- test/core/utils/flattened-tree.js | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/core/utils/flattened-tree.js b/lib/core/utils/flattened-tree.js index d8c1254a81..ac5f3e3153 100644 --- a/lib/core/utils/flattened-tree.js +++ b/lib/core/utils/flattened-tree.js @@ -6,6 +6,8 @@ var axe = axe || { utils: {} }; * 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. elements do not have boxes by default (i.e. they do not get rendered and * their CSS properties are ignored) * 2. elements can be made to have a box by overriding the display property @@ -93,7 +95,7 @@ axe.utils.getFlattenedTree = function (node, shadowId) { } var styl = window.getComputedStyle(node); // check the display property - if (styl.display !== 'contents') { + if (false && styl.display !== 'contents') { // intentionally commented out // has a box retVal = virtualDOMfromNode(node, shadowId); retVal.children = realArray.reduce(reduceShadowDOM, []); diff --git a/test/core/utils/flattened-tree.js b/test/core/utils/flattened-tree.js index eb097f0952..2a9b4373ec 100644 --- a/test/core/utils/flattened-tree.js +++ b/test/core/utils/flattened-tree.js @@ -181,8 +181,9 @@ if (document.body && typeof document.body.attachShadow === 'function') { fixture.querySelectorAll('.stories').forEach(makeShadowTree); }); it('getFlattenedTree\'s virtual DOM should have the elements', function () { - var virtualDOM = axe.utils.getFlattenedTree(fixture); - assert.isTrue(virtualDOM[0].children[1].children[0].children[0].actualNode.nodeName === 'SLOT'); + return; // Chrome's implementation of slot is broken + // var virtualDOM = axe.utils.getFlattenedTree(fixture); + // assert.isTrue(virtualDOM[0].children[1].children[0].children[0].actualNode.nodeName === 'SLOT'); }); }); } From 17d13b11d380c5027538a57efcd3e3633983e4da Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sat, 10 Jun 2017 18:21:15 -0400 Subject: [PATCH 019/142] make is-hidden and contains work with shadow DOM elements --- lib/core/utils/contains.js | 13 +++++++++++ lib/core/utils/is-hidden.js | 5 +++++ test/core/utils/contains.js | 29 +++++++++++++++++++++++++ test/core/utils/is-hidden.js | 42 ++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+) diff --git a/lib/core/utils/contains.js b/lib/core/utils/contains.js index 04b68bc67c..59065c2aef 100644 --- a/lib/core/utils/contains.js +++ b/lib/core/utils/contains.js @@ -8,6 +8,19 @@ axe.utils.contains = function (node, otherNode) { //jshint bitwise: false 'use strict'; + function containsShadowChild(node, otherNode) { + if (node.shadowId === otherNode.shadowId) { + return true; + } + return !!node.children.find((child) => { + return containsShadowChild(child, otherNode); + }); + } + + if ((node.shadowId || otherNode.shadowId) && + node.shadowId !== otherNode.shadowId) { + return containsShadowChild(node, otherNode); + } if (typeof node.actualNode.contains === 'function') { return node.actualNode.contains(otherNode.actualNode); diff --git a/lib/core/utils/is-hidden.js b/lib/core/utils/is-hidden.js index 13e86f437c..68d0259011 100644 --- a/lib/core/utils/is-hidden.js +++ b/lib/core/utils/is-hidden.js @@ -14,6 +14,11 @@ axe.utils.isHidden = function isHidden(el, recursed) { return false; } + // 11 === Node.DOCUMENT_FRAGMENT_NODE + if (el.nodeType === 11) { + el = el.host; // grab the host Node + } + var style = window.getComputedStyle(el, null); if (!style || (!el.parentNode || (style.getPropertyValue('display') === 'none' || diff --git a/test/core/utils/contains.js b/test/core/utils/contains.js index 7cbbefd754..dbaa7d0366 100644 --- a/test/core/utils/contains.js +++ b/test/core/utils/contains.js @@ -57,6 +57,35 @@ describe('axe.utils.contains', function () { assert.isTrue(axe.utils.contains(node1, node2)); }); + it('should work when the child is inside shadow DOM', function () { + var tree, node1, node2; + + function createContentContains() { + var group = document.createElement('div'); + group.innerHTML = ''; + return group; + } + + function makeShadowTreeContains(node) + { + var root = node.attachShadow({mode: 'open'}); + var div = document.createElement('div'); + div.className = 'parent'; + root.appendChild(div); + div.appendChild(createContentContains()); + } + if (document.body && typeof document.body.attachShadow === 'function') { + // shadow DOM v1 - note: v0 is compatible with this code, so no need + // to specifically test this + fixture.innerHTML = '
    '; + makeShadowTreeContains(fixture.firstChild); + tree = axe.utils.getFlattenedTree(fixture.firstChild); + node1 = axe.utils.querySelectorAll(tree, '.parent')[0]; + node2 = axe.utils.querySelectorAll(tree, 'input')[0]; + assert.isTrue(axe.utils.contains(node1, node2)); + } + }); + it('should work', function () { fixture.innerHTML = '
    '; var inner = axe.utils.getFlattenedTree(document.getElementById('inner'))[0]; diff --git a/test/core/utils/is-hidden.js b/test/core/utils/is-hidden.js index 1fad1e0785..2fa915707d 100644 --- a/test/core/utils/is-hidden.js +++ b/test/core/utils/is-hidden.js @@ -1,3 +1,19 @@ +function createContentHidden() { + 'use strict'; + var group = document.createElement('div'); + group.innerHTML = ''; + return group; +} + +function makeShadowTreeHidden(node) { + 'use strict'; + var root = node.attachShadow({mode: 'open'}); + var div = document.createElement('div'); + div.className = 'parent'; + root.appendChild(div); + div.appendChild(createContentHidden()); +} + describe('axe.utils.isHidden', function () { 'use strict'; @@ -52,6 +68,32 @@ describe('axe.utils.isHidden', function () { assert.isFalse(axe.utils.isHidden(el)); }); + it('not hidden: should work when the element is inside shadow DOM', function () { + var tree, node; + + if (document.body && typeof document.body.attachShadow === 'function') { + // shadow DOM v1 - note: v0 is compatible with this code, so no need + // to specifically test this + fixture.innerHTML = '
    '; + makeShadowTreeHidden(fixture.firstChild); + tree = axe.utils.getFlattenedTree(fixture.firstChild); + node = axe.utils.querySelectorAll(tree, 'input')[0]; + assert.isFalse(axe.utils.isHidden(node.actualNode)); + } + }); + it('hidden: should work when the element is inside shadow DOM', function () { + var tree, node; + + if (document.body && typeof document.body.attachShadow === 'function') { + // shadow DOM v1 - note: v0 is compatible with this code, so no need + // to specifically test this + fixture.innerHTML = '
    '; + makeShadowTreeHidden(fixture.firstChild); + tree = axe.utils.getFlattenedTree(fixture.firstChild); + node = axe.utils.querySelectorAll(tree, 'input')[0]; + assert.isTrue(axe.utils.isHidden(node.actualNode)); + } + }); }); \ No newline at end of file From b25117fff7c90460789bd4e2a24938f3124407ac Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sun, 11 Jun 2017 10:58:55 -0400 Subject: [PATCH 020/142] add shadow DOM support to getSelector --- lib/core/utils/get-selector.js | 74 +++++++++++++++++++++------------ test/core/utils/get-selector.js | 47 +++++++++++++++++++++ 2 files changed, 94 insertions(+), 27 deletions(-) diff --git a/lib/core/utils/get-selector.js b/lib/core/utils/get-selector.js index 465e4574d9..852aec3204 100644 --- a/lib/core/utils/get-selector.js +++ b/lib/core/utils/get-selector.js @@ -33,9 +33,9 @@ const commonNodes = [ ]; function getNthChildString (elm, selector) { - const siblings = elm.parentNode && Array.from(elm.parentNode.children || '') || []; + const siblings = elm.parentNode && Array.from(elm.parentNode.children || '') || []; const hasMatchingSiblings = siblings.find(sibling => ( - sibling !== elm && + sibling !== elm && axe.utils.matchesSelector(sibling, selector) )); if (hasMatchingSiblings) { @@ -49,15 +49,16 @@ function getNthChildString (elm, selector) { const createSelector = { // Get ID properties getElmId (elm) { - if (!elm.id) { - return; - } - const id = '#' + escapeSelector(elm.id || ''); + if (!elm.id) { + return; + } + let doc = (elm.getRootNode && elm.getRootNode()) || document; + const id = '#' + escapeSelector(elm.id || ''); if ( - // Don't include youtube's uid values, they change on reload - !id.match(/player_uid_/) && - // Don't include IDs that occur more then once on the page - document.querySelectorAll(id).length === 1 + // Don't include youtube's uid values, they change on reload + !id.match(/player_uid_/) && + // Don't include IDs that occur more then once on the page + doc.querySelectorAll(id).length === 1 ) { return id; } @@ -78,7 +79,7 @@ const createSelector = { // Get uncommon node names getUncommonElm (elm, { isCommonElm, isCustomElm, nodeName }) { if (!isCommonElm && !isCustomElm) { - nodeName = escapeSelector(nodeName); + nodeName = escapeSelector(nodeName); // Add [type] if nodeName is an input element if (nodeName === 'input' && elm.hasAttribute('type')) { nodeName += '[type="' + elm.type + '"]'; @@ -167,18 +168,8 @@ function getElmFeatures (elm, featureCount) { }, []); } -/** - * Gets a unique CSS selector - * @param {HTMLElement} node The element to get the selector for - * @param {Object} optional options - * @return {String} Unique CSS selector for the node - */ -axe.utils.getSelector = function createUniqueSelector (elm, options = {}) { - //todo: implement shadowDOM support +function generateSelector (elm, options, doc) { //jshint maxstatements: 19 - if (!elm) { - return ''; - } let selector, addParent; let { isUnique = false } = options; const idSelector = createSelector.getElmId(elm); @@ -196,25 +187,54 @@ axe.utils.getSelector = function createUniqueSelector (elm, options = {}) { } else { selector = getElmFeatures(elm, featureCount).join(''); selector += getNthChildString(elm, selector); - isUnique = options.isUnique || document.querySelectorAll(selector).length === 1; + isUnique = options.isUnique || doc.querySelectorAll(selector).length === 1; // For the odd case that document doesn't have a unique selector if (!isUnique && elm === document.documentElement) { - selector += ':root'; + // todo: figure out what to do for shadow DOM + selector += ':root'; } addParent = (minDepth !== 0 || !isUnique); } const selectorParts = [selector, ...childSelectors]; - if (elm.parentElement && (toRoot || addParent)) { - return createUniqueSelector(elm.parentNode, { + if (elm.parentElement && elm.parentElement.nodeType !== 11 && + (toRoot || addParent)) { + return generateSelector(elm.parentNode, { toRoot, isUnique, childSelectors: selectorParts, featureCount: 1, minDepth: minDepth -1 - }); + }, doc); } else { return selectorParts.join(' > '); } +} + +/** + * Gets a unique CSS selector + * @param {HTMLElement} node The element to get the selector for + * @param {Object} optional options + * @return {String | Array[String]} Unique CSS selector for the node + */ +axe.utils.getSelector = function createUniqueSelector (elm, options = {}) { + if (!elm) { + return ''; + } + let doc = (elm.getRootNode && elm.getRootNode()) || document; + if (doc.nodeType === 11) { // DOCUMENT_FRAGMENT + let stack = []; + while (doc.nodeType === 11) { + stack.push({elm: elm, doc: doc}); + elm = doc.host; + doc = elm.getRootNode(); + } + stack.push({elm: elm, doc: doc}); + return stack.reverse().map((comp) => { + return generateSelector(comp.elm, options, comp.doc); + }); + } else { + return generateSelector(elm, options, doc); + } }; diff --git a/test/core/utils/get-selector.js b/test/core/utils/get-selector.js index 36f6805290..d5d24dddeb 100644 --- a/test/core/utils/get-selector.js +++ b/test/core/utils/get-selector.js @@ -1,3 +1,19 @@ +function createContentGetSelector() { + 'use strict'; + var group = document.createElement('div'); + group.innerHTML = ''; + return group; +} + +function makeShadowTreeGetSelector(node) { + 'use strict'; + var root = node.attachShadow({mode: 'open'}); + var div = document.createElement('div'); + div.className = 'parent'; + root.appendChild(div); + div.appendChild(createContentGetSelector()); +} + describe('axe.utils.getSelector', function () { 'use strict'; @@ -286,4 +302,35 @@ describe('axe.utils.getSelector', function () { ); }); + it('no options: should work with shadow DOM', function () { + var shadEl; + + if (document.body && typeof document.body.attachShadow === 'function') { + // shadow DOM v1 - note: v0 is compatible with this code, so no need + // to specifically test this + fixture.innerHTML = '
    '; + makeShadowTreeGetSelector(fixture.firstChild); + shadEl = fixture.firstChild.shadowRoot.querySelector('input#myinput'); + assert.deepEqual(axe.utils.getSelector(shadEl), [ + '#fixture > div', + '#myinput' + ]); + } + }); + it('toRoot: should work with shadow DOM', function () { + var shadEl; + + if (document.body && typeof document.body.attachShadow === 'function') { + // shadow DOM v1 - note: v0 is compatible with this code, so no need + // to specifically test this + fixture.innerHTML = '
    '; + makeShadowTreeGetSelector(fixture.firstChild); + shadEl = fixture.firstChild.shadowRoot.querySelector('input#myinput'); + assert.deepEqual(axe.utils.getSelector(shadEl, { toRoot: true }), [ + 'html > body > #fixture > div', + '.parent > div > #myinput' + ]); + } + }); + }); From 2c4c29dc11bb33615c4f3fd0e147a4003430b6a1 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Mon, 12 Jun 2017 18:36:54 -0400 Subject: [PATCH 021/142] get validateAttrValue to work in shadow DOM --- lib/commons/aria/attributes.js | 9 +++++++-- test/commons/aria/attributes.js | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/commons/aria/attributes.js b/lib/commons/aria/attributes.js index 00481cc040..6b4f5b22d7 100644 --- a/lib/commons/aria/attributes.js +++ b/lib/commons/aria/attributes.js @@ -43,13 +43,18 @@ aria.validateAttr = function (att) { * @return {Boolean} */ aria.validateAttrValue = function (node, attr) { - //jshint maxcomplexity: 12 + //jshint maxcomplexity: 13 'use strict'; var matches, list, - doc = document, value = node.getAttribute(attr), attrInfo = lookupTables.attributes[attr]; + var doc = (node.getRootNode && node.getRootNode()) || document; // this is for backwards compatibility + if (doc === node) { + // disconnected node + doc = document; + } + if (!attrInfo) { return true; } diff --git a/test/commons/aria/attributes.js b/test/commons/aria/attributes.js index 88f947b4b8..590ee850ff 100644 --- a/test/commons/aria/attributes.js +++ b/test/commons/aria/attributes.js @@ -1,4 +1,3 @@ - describe('aria.requiredAttr', function () { 'use strict'; @@ -113,6 +112,24 @@ describe('aria.validateAttr', function () { }); }); +function createContentVAV() { + 'use strict'; + var group = document.createElement('div'); + group.innerHTML = '' + + '' + + ''; + return group; +} + +function makeShadowTreeVAV(node) { + 'use strict'; + var root = node.attachShadow({mode: 'open'}); + var div = document.createElement('div'); + div.className = 'parent'; + root.appendChild(div); + div.appendChild(createContentVAV()); +} + describe('aria.validateAttrValue', function () { 'use strict'; @@ -193,6 +210,20 @@ describe('aria.validateAttrValue', function () { node.setAttribute('cats', 'invalid'); assert.isFalse(axe.commons.aria.validateAttrValue(node, 'cats')); }); + it('should work in shadow DOM', function () { + var shadEl; + + if (document.body && typeof document.body.attachShadow === 'function') { + // shadow DOM v1 - note: v0 is compatible with this code, so no need + // to specifically test this + fixture.innerHTML = '
    '; + makeShadowTreeVAV(fixture.firstChild); + shadEl = fixture.firstChild.shadowRoot.querySelector('input#myinput'); + assert.isTrue(axe.commons.aria.validateAttrValue(shadEl, 'aria-labelledby')); + shadEl = fixture.firstChild.shadowRoot.querySelector('input#invalid'); + assert.isFalse(axe.commons.aria.validateAttrValue(shadEl, 'aria-labelledby')); + } + }); }); describe('idrefs', function () { From 14cb707d6d1903d11ad7c52a29682b34304dc8dc Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Tue, 13 Jun 2017 07:34:08 -0400 Subject: [PATCH 022/142] change the documentation to show an example of the JSON and fix other indentation problems with the lists --- doc/API.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/doc/API.md b/doc/API.md index fe8c3bd8b0..e721f89732 100644 --- a/doc/API.md +++ b/doc/API.md @@ -495,8 +495,8 @@ axe.run(document, function(err, results) { * `help` - `"Elements must have sufficient color contrast"` * `helpUrl` - `"https://dequeuniversity.com/courses/html-css/visual-layout/color-contrast"` * `id` - `"color-contrast"` - * `nodes` - * `target[0]` - `"#js_off-canvas-wrap > .inner-wrap >.kinja-title.proxima.js_kinja-title-desktop"` + * `nodes` + * `target[0]` - `"#js_off-canvas-wrap > .inner-wrap >.kinja-title.proxima.js_kinja-title-desktop"` * `passes[1]` ... @@ -507,9 +507,9 @@ axe.run(document, function(err, results) { * `help` - `"'; var target = fixture.querySelector('button'); - assert.isTrue(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isTrue(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match '; var target = fixture.querySelector('button'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match ', function () { fixture.innerHTML = ''; var target = fixture.querySelector('button'); - assert.isFalse(rule.matches(target.querySelector('span'))); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target.querySelector('span'), axe.utils.getNodeFromTree(tree[0], target.querySelector('span')))); }); it('should not match ', function () { fixture.innerHTML = ''; var target = fixture.querySelector('button'); - assert.isFalse(rule.matches(target.querySelector('i'))); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target.querySelector('i'), axe.utils.getNodeFromTree(tree[0], target.querySelector('i')))); }); it('should not match ', function () { fixture.innerHTML = ''; var target = fixture.querySelector('input'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match a disabled input\'s label - explicit label', function () { fixture.innerHTML = ''; var target = fixture.querySelector('label'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match a disabled input\'s label - implicit label (input)', function () { fixture.innerHTML = ''; var target = fixture.querySelector('label'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match a disabled input\'s label - implicit label (textarea)', function () { fixture.innerHTML = ''; var target = fixture.querySelector('label'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match a disabled input\'s label - implicit label (select)', function () { fixture.innerHTML = ''; var target = fixture.querySelector('label'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match a disabled input\'s label - aria-labelledby', function () { fixture.innerHTML = '
    Test
    '; var target = fixture.querySelector('div'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match aria-disabled=true', function () { fixture.innerHTML = '
    hi
    '; var target = fixture.querySelector('div'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match a descendant of aria-disabled=true', function () { fixture.innerHTML = '
    hi
    '; var target = fixture.querySelector('span'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match a descendant of a disabled fieldset', function () { fixture.innerHTML = '
    '; var target = fixture.querySelector('label'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match a descendant of an explicit label for a disabled input', function () { fixture.innerHTML = ''; var target = fixture.querySelector('span'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); it('should not match a descendant of an implicit label for a disabled input', function () { fixture.innerHTML = ''; var target = fixture.querySelector('span'); - assert.isFalse(rule.matches(target)); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(target, axe.utils.getNodeFromTree(tree[0], target))); }); - (shadowSupport ? it : xit) - ('should match a descendant of an element across a shadow boundary', function () { - fixture.innerHTML = '
    ' + + if (shadowSupport) { + it('should match a descendant of an element across a shadow boundary', function () { + fixture.innerHTML = '
    ' + + '
    '; + + var shadowRoot = document.getElementById('parent').attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '
    Text
    '; + + var shadowTarget = fixture.firstChild.shadowRoot.querySelector('#shadowTarget'); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isTrue(rule.matches(shadowTarget, axe.utils.getNodeFromTree(tree[0], shadowTarget))); + }); + + it('should look at the correct root node when looking up an explicit label and disabled input', function () { + fixture.innerHTML = '
    '+ + '' + '
    '; - var shadowRoot = document.getElementById('parent').attachShadow({ mode: 'open' }); - shadowRoot.innerHTML = '
    Text
    '; + var shadowRoot = document.getElementById('parent').attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '
    ' + + '' + + '' + + '
    '; - var shadowTarget = fixture.firstChild.shadowRoot.querySelector('#shadowTarget'); - var tree = axe._tree = axe.utils.getFlattenedTree(fixture); - assert.isTrue(rule.matches(shadowTarget, axe.utils.getNodeFromTree(tree[0], shadowTarget))); - }); + var shadowLabel = fixture.firstChild.shadowRoot.querySelector('#shadowLabel'); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(shadowLabel, axe.utils.getNodeFromTree(tree[0], shadowLabel))); + }); + + it('should look at the correct root node when looking up implicit label and disabled input', function () { + fixture.innerHTML = '
    '+ + '' + + '
    '; + + var shadowRoot = document.getElementById('parent').attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '
    ' + + '' + + '
    '; + + var shadowLabel = fixture.firstChild.shadowRoot.querySelector('#shadowLabel'); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(shadowLabel, axe.utils.getNodeFromTree(tree[0], shadowLabel))); + }); + + it('should look at the correct root node for a disabled control\'s label associated w/ aria-labelledby', function () { + fixture.innerHTML = '
    '+ + '' + + '
    '; + + var shadowRoot = document.getElementById('parent').attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '
    ' + + '' + + '' + + '
    '; + + var shadowLabel = fixture.firstChild.shadowRoot.querySelector('#shadowLabel'); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isFalse(rule.matches(shadowLabel, axe.utils.getNodeFromTree(tree[0], shadowLabel))); + }); + + it('should look at the children of a virtual node for overlap', function () { + fixture.innerHTML = '
    '+ + '
    ' + + '
    '; + + var shadowRoot = document.getElementById('firstChild').attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = 'Some text' + + '

    Other text

    '; + + var firstChild = fixture.querySelector('#firstChild'); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isTrue(rule.matches(firstChild, axe.utils.getNodeFromTree(tree[0], firstChild))); + }); + } }); From b28597c4ea5b035fcd2900e48eddcea691e9b945 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Tue, 11 Jul 2017 15:15:23 -0700 Subject: [PATCH 068/142] fix: ensure document is fetched from correct node --- lib/rules/color-contrast-matches.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/rules/color-contrast-matches.js b/lib/rules/color-contrast-matches.js index 04ee5fdebf..522d0e589e 100644 --- a/lib/rules/color-contrast-matches.js +++ b/lib/rules/color-contrast-matches.js @@ -1,8 +1,7 @@ /* global document */ var nodeName = node.nodeName.toUpperCase(), - nodeType = node.type, - doc = axe.commons.dom.getRootNode(node); + nodeType = node.type; if (node.getAttribute('aria-disabled') === 'true' || axe.commons.dom.findUp(node, '[aria-disabled="true"]')) { return false; @@ -40,6 +39,7 @@ if (nodeName === 'LABEL' || nodeParentLabel) { relevantNode = nodeParentLabel; } // explicit label of disabled input + let doc = axe.commons.dom.getRootNode(relevantNode); var candidate = relevantNode.htmlFor && doc.getElementById(relevantNode.htmlFor); if (candidate && candidate.disabled) { return false; @@ -55,6 +55,7 @@ if (nodeName === 'LABEL' || nodeParentLabel) { // label of disabled control associated w/ aria-labelledby if (node.id) { + let doc = axe.commons.dom.getRootNode(node); var candidate = doc.querySelector('[aria-labelledby~=' + axe.commons.utils.escapeSelector(node.id) + ']'); if (candidate && candidate.disabled) { return false; From 615170eda9566aa5f29f530440b97d3c6139a50c Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Tue, 11 Jul 2017 15:44:26 -0700 Subject: [PATCH 069/142] test: add test for shadow boundary, fix implicit --- test/rule-matches/color-contrast-matches.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test/rule-matches/color-contrast-matches.js b/test/rule-matches/color-contrast-matches.js index c3f0e4a10e..4488c5805f 100644 --- a/test/rule-matches/color-contrast-matches.js +++ b/test/rule-matches/color-contrast-matches.js @@ -302,13 +302,12 @@ describe('color-contrast-matches', function () { it('should look at the correct root node when looking up implicit label and disabled input', function () { fixture.innerHTML = '
    '+ - '' + + '' + '
    '; var shadowRoot = document.getElementById('parent').attachShadow({ mode: 'open' }); shadowRoot.innerHTML = '
    ' + - '' + + '' + '
    '; var shadowLabel = fixture.firstChild.shadowRoot.querySelector('#shadowLabel'); @@ -332,6 +331,18 @@ describe('color-contrast-matches', function () { assert.isFalse(rule.matches(shadowLabel, axe.utils.getNodeFromTree(tree[0], shadowLabel))); }); + it('should handle input/label spread across the shadow boundary', function () { + fixture.innerHTML = ''; + + var container = document.getElementById('firstChild'); + var shadowRoot = container.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = ''; + + var shadowTarget = container.shadowRoot.querySelector('#shadowTarget'); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isTrue(rule.matches(shadowTarget, axe.utils.getNodeFromTree(tree[0], shadowTarget))); + }); + it('should look at the children of a virtual node for overlap', function () { fixture.innerHTML = '
    '+ '
    ' + From 3a5be921000ef21475798d2a93957c1366811b56 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Thu, 13 Jul 2017 10:01:14 -0700 Subject: [PATCH 070/142] test: make input disabled, add slotted example --- test/rule-matches/color-contrast-matches.js | 26 ++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/test/rule-matches/color-contrast-matches.js b/test/rule-matches/color-contrast-matches.js index 4488c5805f..77979cad30 100644 --- a/test/rule-matches/color-contrast-matches.js +++ b/test/rule-matches/color-contrast-matches.js @@ -336,11 +336,11 @@ describe('color-contrast-matches', function () { var container = document.getElementById('firstChild'); var shadowRoot = container.attachShadow({ mode: 'open' }); - shadowRoot.innerHTML = ''; + shadowRoot.innerHTML = ''; - var shadowTarget = container.shadowRoot.querySelector('#shadowTarget'); + var shadowTarget = container.shadowRoot.querySelector('#input'); var tree = axe._tree = axe.utils.getFlattenedTree(fixture); - assert.isTrue(rule.matches(shadowTarget, axe.utils.getNodeFromTree(tree[0], shadowTarget))); + assert.isFalse(rule.matches(shadowTarget, axe.utils.getNodeFromTree(tree[0], shadowTarget))); }); it('should look at the children of a virtual node for overlap', function () { @@ -356,5 +356,25 @@ describe('color-contrast-matches', function () { var tree = axe._tree = axe.utils.getFlattenedTree(fixture); assert.isTrue(rule.matches(firstChild, axe.utils.getNodeFromTree(tree[0], firstChild))); }); + + it('should handle an input added through slotted content', function () { + fixture.innerHTML = ''; + + function createContentSlotted() { + var group = document.createElement('span'); + group.innerHTML = ''; + return group; + } + + var slotted = fixture.querySelector('.slotted'); + var shadowRoot = slotted.attachShadow({mode: 'open'}); + shadowRoot.appendChild(createContentSlotted()); + + var input = slotted.querySelector('input'); + var tree = axe._tree = axe.utils.getFlattenedTree(fixture); + assert.isTrue(rule.matches(input, axe.utils.getNodeFromTree(tree[0], input))); + }); } }); From 4534e86f622be594f6b3379efff11dee0b2474d8 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Mon, 10 Jul 2017 14:57:24 -0700 Subject: [PATCH 071/142] fix: pass virtualNode to Rule.run Closes https://github.com/dequelabs/axe-core/issues/394 --- lib/core/base/rule.js | 2 +- test/core/base/rule.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js index 35168a8cec..44ed7e4e3c 100644 --- a/lib/core/base/rule.js +++ b/lib/core/base/rule.js @@ -134,7 +134,7 @@ Rule.prototype.run = function (context, options, resolve, reject) { try { // Matches throws an error when it lacks support for document methods nodes = this.gather(context) - .filter(node => this.matches(node.actualNode)); + .filter(node => this.matches(node.actualNode, node)); } catch (error) { // Exit the rule execution if matches fails reject(new SupportError({cause: error, ruleId: this.id})); diff --git a/test/core/base/rule.js b/test/core/base/rule.js index 92138cc004..d1b58b4fa2 100644 --- a/test/core/base/rule.js +++ b/test/core/base/rule.js @@ -143,7 +143,26 @@ describe('Rule', function() { assert.isTrue(success); done(); }, isNotCalled); + }); + + it('should pass a virtualNode to #matches', function(done) { + var div = document.createElement('div'); + fixture.appendChild(div); + var success = false, + rule = new Rule({ + matches: function(node, virtualNode) { + assert.equal(virtualNode.actualNode, div); + success = true; + return []; + } + }); + rule.run({ + include: [axe.utils.getFlattenedTree(div)[0]] + }, {}, function() { + assert.isTrue(success); + done(); + }, isNotCalled); }); it('should handle an error in #matches', function(done) { From d02dba3223fefe525438330e40b5da5de81eeeb5 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Fri, 14 Jul 2017 03:01:09 -0700 Subject: [PATCH 072/142] test: add unit tests for button-has-visible-text (#434) Closes https://github.com/dequelabs/axe-core/issues/419 --- test/checks/shared/button-has-visible-text.js | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 test/checks/shared/button-has-visible-text.js diff --git a/test/checks/shared/button-has-visible-text.js b/test/checks/shared/button-has-visible-text.js new file mode 100644 index 0000000000..fb3d0fa565 --- /dev/null +++ b/test/checks/shared/button-has-visible-text.js @@ -0,0 +1,47 @@ +describe('button-has-visible-text', function () { + 'use strict'; + + var fixture = document.getElementById('fixture'); + + var checkContext = { + _data: null, + data: function (d) { + this._data = d; + } + }; + + afterEach(function () { + fixture.innerHTML = ''; + checkContext._data = null; + }); + + it('should return false if button element is empty', function () { + fixture.innerHTML = ''; + + var node = fixture.querySelector('button'); + assert.isFalse(checks['button-has-visible-text'].evaluate.call(checkContext, node)); + }); + + it('should return true if a button element has text', function () { + fixture.innerHTML = ''; + + var node = fixture.querySelector('button'); + assert.isTrue(checks['button-has-visible-text'].evaluate.call(checkContext, node)); + assert.deepEqual(checkContext._data, 'Name'); + }); + + it('should return true if ARIA button has text', function () { + fixture.innerHTML = '
    Text
    '; + + var node = fixture.querySelector('div'); + assert.isTrue(checks['button-has-visible-text'].evaluate.call(checkContext, node)); + assert.deepEqual(checkContext._data, 'Text'); + }); + + it('should return false if ARIA button has no text', function () { + fixture.innerHTML = '
    '; + + var node = fixture.querySelector('div'); + assert.isFalse(checks['button-has-visible-text'].evaluate.call(checkContext, node)); + }); +}); \ No newline at end of file From 6f5327918ca40a4eebfd45533d51129ae72f8d0f Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 14 Jul 2017 13:52:33 +0200 Subject: [PATCH 073/142] feat: add check testUtils --- test/.jshintrc | 1 + test/testutils.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/test/.jshintrc b/test/.jshintrc index dce6c75bef..d8372a2fbc 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -3,6 +3,7 @@ "globals": { "describe": true, "it": true, + "xit": true, "before": true, "beforeEach": true, "after": true, diff --git a/test/testutils.js b/test/testutils.js index d7cab92b85..07508a5b33 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -16,4 +16,35 @@ testUtils.shadowSupport = (function(document) { })(document); +testUtils.fixtureSetup = function (content) { + 'use strict'; + var fixture = document.querySelector('#fixture'); + if (typeof content === 'string') { + fixture.innerHTML = content; + } else if (content instanceof Node) { + fixture.appendChild(content); + } + axe._tree = axe.utils.getFlattenedTree(fixture); + return fixture; +}; + +/** + * Create check arguments + * + * @param Node|String Stuff to go into the fixture (html or node) + * @param Object Options argument for the check (optional, default: {}) + * @param String Target for the check, CSS selector (default: '#target') + */ +testUtils.checkSetup = function (content, options, target) { + 'use strict'; + // Normalize the params + if (typeof options !== 'object') { + target = options; + options = {}; + } + testUtils.fixtureSetup(content); + var node = axe.utils.querySelectorAll(axe._tree[0], target || '#target')[0]; + return [node.actualNode, options, node]; +}; + axe.testUtils = testUtils; \ No newline at end of file From 0f215746cd98b9a7280c6e22d65689a6a04cf133 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 14 Jul 2017 13:53:29 +0200 Subject: [PATCH 074/142] feat: ShadowDOM support for media checks --- lib/checks/media/caption.js | 17 +++++++--------- lib/checks/media/description.js | 18 ++++++++-------- test/checks/media/caption.js | 28 +++++++++++++++---------- test/checks/media/description.js | 35 +++++++++++++++++++------------- 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/lib/checks/media/caption.js b/lib/checks/media/caption.js index 65bd8c8871..80b6af6372 100644 --- a/lib/checks/media/caption.js +++ b/lib/checks/media/caption.js @@ -1,13 +1,10 @@ -var tracks = node.querySelectorAll('track'); +var tracks = axe.utils.querySelectorAll(virtualNode, 'track'); + if (tracks.length) { - for (var i=0; i ( + actualNode.getAttribute('kind').toLowerCase() === 'captions' + )); } -// for multiple track elements, return the first one that matches +// Undefined if there are no tracks - media may be decorative return undefined; diff --git a/lib/checks/media/description.js b/lib/checks/media/description.js index 8f57000b94..65e29cd3b2 100644 --- a/lib/checks/media/description.js +++ b/lib/checks/media/description.js @@ -1,12 +1,12 @@ -var tracks = node.querySelectorAll('track'); +var tracks = axe.utils.querySelectorAll(virtualNode, 'track'); + if (tracks.length) { - for (var i=0; i ( + actualNode.getAttribute('kind').toLowerCase() === 'descriptions' + )); + axe.log(tracks.map(t => t.actualNode.getAttribute('kind')), out); + return out; } +// Undefined if there are no tracks - media may be decorative return undefined; diff --git a/test/checks/media/caption.js b/test/checks/media/caption.js index c6e49af73a..3c313ed72c 100644 --- a/test/checks/media/caption.js +++ b/test/checks/media/caption.js @@ -2,29 +2,35 @@ describe('caption', function () { 'use strict'; var fixture = document.getElementById('fixture'); + var shadowSupport = axe.testUtils.shadowSupport; + var checkSetup = axe.testUtils.checkSetup; afterEach(function () { fixture.innerHTML = ''; }); it('should return undefined if there is no track element', function () { - fixture.innerHTML = ''; - var node = fixture.querySelector('audio'); - - assert.isUndefined(checks.caption.evaluate(node)); + var checkArgs = checkSetup('', 'audio'); + assert.isUndefined(checks.caption.evaluate.apply(null, checkArgs)); }); it('should fail if there is no kind=captions attribute', function () { - fixture.innerHTML = ''; - var node = fixture.querySelector('audio'); - - assert.isTrue(checks.caption.evaluate(node)); + var checkArgs = checkSetup('', 'audio'); + assert.isTrue(checks.caption.evaluate.apply(null, checkArgs)); }); it('should pass if there is a kind=captions attribute', function () { - fixture.innerHTML = ''; - var node = fixture.querySelector('audio'); + var checkArgs = checkSetup('', 'audio'); + assert.isFalse(checks.caption.evaluate.apply(null, checkArgs)); + }); + + (shadowSupport.v1 ? it : xit)('should get track from composed tree', function () { + var node = document.createElement('div'); + node.innerHTML = ''; + var shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = ''; - assert.isFalse(checks.caption.evaluate(node)); + var checkArgs = checkSetup(node, {}, 'audio'); + assert.isTrue(checks.caption.evaluate.apply(null, checkArgs)); }); }); diff --git a/test/checks/media/description.js b/test/checks/media/description.js index 168b1120f7..a423e1f98b 100644 --- a/test/checks/media/description.js +++ b/test/checks/media/description.js @@ -1,30 +1,37 @@ describe('description', function () { 'use strict'; - var fixture = document.getElementById('fixture'); + var shadowSupport = axe.testUtils.shadowSupport; + var checkSetup = axe.testUtils.checkSetup; afterEach(function () { - fixture.innerHTML = ''; + document.getElementById('fixture').innerHTML = ''; }); it('should return undefined if there is no track element', function () { - fixture.innerHTML = ''; - var node = fixture.querySelector('video'); - - assert.isUndefined(checks.description.evaluate(node)); + var checkArgs = checkSetup('', 'video'); + assert.isUndefined(checks.description.evaluate.apply(null, checkArgs)); }); - it('should fail if there is no kind=descriptions attribute', function () { - fixture.innerHTML = ''; - var node = fixture.querySelector('video'); - - assert.isTrue(checks.description.evaluate(node)); + it('should fail if there is no kind=captions attribute', function () { + var checkArgs = checkSetup('', 'video'); + assert.isTrue(checks.description.evaluate.apply(null, checkArgs)); }); it('should pass if there is a kind=descriptions attribute', function () { - fixture.innerHTML = ''; - var node = fixture.querySelector('video'); + var checkArgs = checkSetup('', 'video'); + assert.isFalse(checks.description.evaluate.apply(null, checkArgs)); + }); - assert.isFalse(checks.description.evaluate(node)); + (shadowSupport.v1 ? it : xit)('should get track from composed tree', function () { + var node = document.createElement('div'); + node.innerHTML = ''; + var shadow = node.attachShadow({ mode: 'open' }); + shadow.innerHTML = ''; + + var checkArgs = checkSetup(node, {}, 'video'); + axe.log(checkArgs); + assert.isFalse(checks.description.evaluate.apply(null, checkArgs)); }); + }); From f996d0f1b77ca2e03b4b0d384d911adc7bec48a7 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Fri, 14 Jul 2017 22:03:06 +0200 Subject: [PATCH 075/142] fix: Allow to have no kind attribute --- lib/checks/media/caption.js | 2 +- lib/checks/media/description.js | 2 +- test/checks/media/caption.js | 5 +++++ test/checks/media/description.js | 5 +++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/checks/media/caption.js b/lib/checks/media/caption.js index 80b6af6372..d7d6090521 100644 --- a/lib/checks/media/caption.js +++ b/lib/checks/media/caption.js @@ -3,7 +3,7 @@ var tracks = axe.utils.querySelectorAll(virtualNode, 'track'); if (tracks.length) { // return false if any track has kind === 'caption' return !tracks.some(({ actualNode }) => ( - actualNode.getAttribute('kind').toLowerCase() === 'captions' + (actualNode.getAttribute('kind') || '').toLowerCase() === 'captions' )); } // Undefined if there are no tracks - media may be decorative diff --git a/lib/checks/media/description.js b/lib/checks/media/description.js index 65e29cd3b2..f6bb01a5fb 100644 --- a/lib/checks/media/description.js +++ b/lib/checks/media/description.js @@ -3,7 +3,7 @@ var tracks = axe.utils.querySelectorAll(virtualNode, 'track'); if (tracks.length) { // return false if any track has kind === 'description' var out = !tracks.some(({ actualNode }) => ( - actualNode.getAttribute('kind').toLowerCase() === 'descriptions' + (actualNode.getAttribute('kind') || '').toLowerCase() === 'descriptions' )); axe.log(tracks.map(t => t.actualNode.getAttribute('kind')), out); return out; diff --git a/test/checks/media/caption.js b/test/checks/media/caption.js index 3c313ed72c..3d7606a644 100644 --- a/test/checks/media/caption.js +++ b/test/checks/media/caption.js @@ -19,6 +19,11 @@ describe('caption', function () { assert.isTrue(checks.caption.evaluate.apply(null, checkArgs)); }); + it('should fail if there is no kind attribute', function () { + var checkArgs = checkSetup('', 'video'); + assert.isTrue(checks.description.evaluate.apply(null, checkArgs)); + }); + it('should pass if there is a kind=captions attribute', function () { var checkArgs = checkSetup('', 'audio'); assert.isFalse(checks.caption.evaluate.apply(null, checkArgs)); diff --git a/test/checks/media/description.js b/test/checks/media/description.js index a423e1f98b..46512a2e5a 100644 --- a/test/checks/media/description.js +++ b/test/checks/media/description.js @@ -18,6 +18,11 @@ describe('description', function () { assert.isTrue(checks.description.evaluate.apply(null, checkArgs)); }); + it('should fail if there is no kind attribute', function () { + var checkArgs = checkSetup('', 'video'); + assert.isTrue(checks.description.evaluate.apply(null, checkArgs)); + }); + it('should pass if there is a kind=descriptions attribute', function () { var checkArgs = checkSetup('', 'video'); assert.isFalse(checks.description.evaluate.apply(null, checkArgs)); From d92c1a14379ff6592115fb7085fb788bbe00c397 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Sun, 16 Jul 2017 15:49:55 +0200 Subject: [PATCH 076/142] feat: Add shadow DOM support to list checks (#439) * feat: add check testUtils * fix: getComposedParent should not return slot nodes * feat: Add shadow DOM support to list checks * test: Add shadow DOM list check fails --- lib/checks/lists/dlitem.js | 4 +- lib/checks/lists/has-listitem.js | 11 +-- lib/checks/lists/listitem.js | 10 +- lib/checks/lists/only-dlitems.js | 19 ++-- lib/checks/lists/only-listitems.js | 19 ++-- lib/checks/lists/structured-dlitems.js | 4 +- lib/commons/dom/get-composed-parent.js | 5 +- test/checks/lists/dlitem.js | 30 ++++-- test/checks/lists/has-listitem.js | 44 +++++---- test/checks/lists/listitem.js | 44 ++++++--- test/checks/lists/only-dlitems.js | 118 ++++++++++++------------ test/checks/lists/only-listitems.js | 108 +++++++++++----------- test/checks/lists/structured-dlitems.js | 67 +++++++------- test/commons/dom/get-composed-parent.js | 8 +- 14 files changed, 258 insertions(+), 233 deletions(-) diff --git a/lib/checks/lists/dlitem.js b/lib/checks/lists/dlitem.js index 96ec9e65b1..80a6608fc7 100644 --- a/lib/checks/lists/dlitem.js +++ b/lib/checks/lists/dlitem.js @@ -1,2 +1,2 @@ -return node.parentNode.tagName === 'DL'; - +var parent = axe.commons.dom.getComposedParent(node); +return parent.nodeName.toUpperCase() === 'DL'; diff --git a/lib/checks/lists/has-listitem.js b/lib/checks/lists/has-listitem.js index d38f7bd805..05aff66304 100644 --- a/lib/checks/lists/has-listitem.js +++ b/lib/checks/lists/has-listitem.js @@ -1,9 +1,2 @@ -var children = node.children; -if (children.length === 0) { return true; } - -for (var i = 0; i < children.length; i++) { - if (children[i].nodeName.toUpperCase() === 'LI') { return false; } -} - -return true; - +return virtualNode.children.every(({ actualNode }) => + actualNode.nodeName.toUpperCase() !== 'LI'); diff --git a/lib/checks/lists/listitem.js b/lib/checks/lists/listitem.js index 16e396b878..1a7abcb40d 100644 --- a/lib/checks/lists/listitem.js +++ b/lib/checks/lists/listitem.js @@ -1,6 +1,4 @@ - -if (['UL', 'OL'].indexOf(node.parentNode.nodeName.toUpperCase()) !== -1) { - return true; -} - -return node.parentNode.getAttribute('role') === 'list'; +var parent = axe.commons.dom.getComposedParent(node); +return (['UL', 'OL'].includes(parent.nodeName.toUpperCase()) || + (parent.getAttribute('role') || '').toLowerCase() === 'list'); + \ No newline at end of file diff --git a/lib/checks/lists/only-dlitems.js b/lib/checks/lists/only-dlitems.js index a003f8dbee..ff3a20d62d 100644 --- a/lib/checks/lists/only-dlitems.js +++ b/lib/checks/lists/only-dlitems.js @@ -1,19 +1,16 @@ -var child, - nodeName, - bad = [], - children = node.childNodes, +var bad = [], permitted = ['STYLE', 'META', 'LINK', 'MAP', 'AREA', 'SCRIPT', 'DATALIST', 'TEMPLATE'], hasNonEmptyTextNode = false; -for (var i = 0; i < children.length; i++) { - child = children[i]; - var nodeName = child.nodeName.toUpperCase(); - if (child.nodeType === 1 && nodeName !== 'DT' && nodeName !== 'DD' && permitted.indexOf(nodeName) === -1) { - bad.push(child); - } else if (child.nodeType === 3 && child.nodeValue.trim() !== '') { +virtualNode.children.forEach(({ actualNode }) => { + var nodeName = actualNode.nodeName.toUpperCase(); + if (actualNode.nodeType === 1 && nodeName !== 'DT' && nodeName !== 'DD' && permitted.indexOf(nodeName) === -1) { + bad.push(actualNode); + } else if (actualNode.nodeType === 3 && actualNode.nodeValue.trim() !== '') { hasNonEmptyTextNode = true; } -} +}); + if (bad.length) { this.relatedNodes(bad); } diff --git a/lib/checks/lists/only-listitems.js b/lib/checks/lists/only-listitems.js index 547fe79652..bdb53064e5 100644 --- a/lib/checks/lists/only-listitems.js +++ b/lib/checks/lists/only-listitems.js @@ -1,19 +1,16 @@ -var child, - nodeName, - bad = [], - children = node.childNodes, +var bad = [], permitted = ['STYLE', 'META', 'LINK', 'MAP', 'AREA', 'SCRIPT', 'DATALIST', 'TEMPLATE'], hasNonEmptyTextNode = false; -for (var i = 0; i < children.length; i++) { - child = children[i]; - nodeName = child.nodeName.toUpperCase(); - if (child.nodeType === 1 && nodeName !== 'LI' && permitted.indexOf(nodeName) === -1) { - bad.push(child); - } else if (child.nodeType === 3 && child.nodeValue.trim() !== '') { +virtualNode.children.forEach(({ actualNode }) => { + var nodeName = actualNode.nodeName.toUpperCase(); + if (actualNode.nodeType === 1 && nodeName !== 'LI' && permitted.indexOf(nodeName) === -1) { + bad.push(actualNode); + } else if (actualNode.nodeType === 3 && actualNode.nodeValue.trim() !== '') { hasNonEmptyTextNode = true; } -} +}); + if (bad.length) { this.relatedNodes(bad); } diff --git a/lib/checks/lists/structured-dlitems.js b/lib/checks/lists/structured-dlitems.js index b8578dffb1..7654da061d 100644 --- a/lib/checks/lists/structured-dlitems.js +++ b/lib/checks/lists/structured-dlitems.js @@ -1,9 +1,9 @@ -var children = node.children; +var children = virtualNode.children; if ( !children || !children.length) { return false; } var hasDt = false, hasDd = false, nodeName; for (var i = 0; i < children.length; i++) { - nodeName = children[i].nodeName.toUpperCase(); + nodeName = children[i].actualNode.nodeName.toUpperCase(); if (nodeName === 'DT') { hasDt = true; } if (hasDt && nodeName === 'DD') { return false; } if (nodeName === 'DD') { hasDd = true; } diff --git a/lib/commons/dom/get-composed-parent.js b/lib/commons/dom/get-composed-parent.js index c5cfc74339..fdec0a4b18 100644 --- a/lib/commons/dom/get-composed-parent.js +++ b/lib/commons/dom/get-composed-parent.js @@ -6,7 +6,10 @@ */ dom.getComposedParent = function getComposedParent (element) { if (element.assignedSlot) { - return element.assignedSlot; // content of a shadow DOM slot + // NOTE: If the display of a slot element isn't 'contents', + // the slot shouldn't be ignored. Chrome does not support this (yet) so, + // we'll skip this part for now. + return getComposedParent(element.assignedSlot); // content of a shadow DOM slot } else if (element.parentNode) { var parentNode = element.parentNode; if (parentNode.nodeType === 1) { diff --git a/test/checks/lists/dlitem.js b/test/checks/lists/dlitem.js index f8d50778c8..827e6a6405 100644 --- a/test/checks/lists/dlitem.js +++ b/test/checks/lists/dlitem.js @@ -2,26 +2,42 @@ describe('dlitem', function () { 'use strict'; var fixture = document.getElementById('fixture'); + var checkSetup = axe.testUtils.checkSetup; + var shadowSupport = axe.testUtils.shadowSupport; afterEach(function () { fixture.innerHTML = ''; }); it('should pass if the dlitem has a parent
    ', function () { - fixture.innerHTML = '
    My list item
    '; - var node = fixture.querySelector('#target'); + var checkArgs = checkSetup('
    My list item
    '); - assert.isTrue(checks.dlitem.evaluate(node)); + assert.isTrue(checks.dlitem.evaluate.apply(null, checkArgs)); + }); + it('should fail if the dlitem has an incorrect parent', function () { + var checkArgs = checkSetup('
    '; - var node = fixture.querySelector('#target'); - assert.isFalse(checks['only-dlitems'].evaluate.call(checkContext, node)); + var checkArgs = checkSetup('
    A list
    '); + + assert.isFalse(checks['only-dlitems'].evaluate.apply(checkContext, checkArgs)); }); it('should return false if
    A list
    '; - var node = fixture.querySelector('#target'); - assert.isFalse(checks['only-dlitems'].evaluate.call(checkContext, node)); + var checkArgs = checkSetup('
    A list
    '); + + assert.isFalse(checks['only-dlitems'].evaluate.apply(checkContext, checkArgs)); }); it('should return false if