';
- 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],
- 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 = '
';
+ 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 link = document.getElementById('link');
assert.isTrue(axe.commons.dom.isInTextBlock(link));
});
it('returns false if the element is a block', function () {
- fixture.innerHTML =
+ fixtureSetup(
'
';
+ '');
var link = document.getElementById('link');
assert.isFalse(axe.commons.dom.isInTextBlock(link));
});
it('returns false if the element has the only text in the block', function () {
- fixture.innerHTML =
+ fixtureSetup(
'
';
+ '');
var link = document.getElementById('link');
assert.isFalse(axe.commons.dom.isInTextBlock(link));
});
it('returns false if there is more text in link(s) than in the rest of the block', function () {
- fixture.innerHTML =
+ fixtureSetup(
'
';
+ '');
var link = document.getElementById('link');
assert.isFalse(axe.commons.dom.isInTextBlock(link));
});
it('return false if there are links along side other links', function () {
- fixture.innerHTML =
+ fixtureSetup(
'
';
+ '');
var link = document.getElementById('link');
assert.isFalse(axe.commons.dom.isInTextBlock(link));
});
it('ignore text in the block coming before a br', function () {
- fixture.innerHTML =
+ fixtureSetup(
'
';
+ '');
var link = document.getElementById('link');
assert.isFalse(axe.commons.dom.isInTextBlock(link));
});
it('ignore text in the block coming after a br', function () {
- fixture.innerHTML =
+ fixtureSetup(
'
';
+ '');
var link = document.getElementById('link');
assert.isFalse(axe.commons.dom.isInTextBlock(link));
});
it('ignore text in the block coming before and after a br', function () {
- fixture.innerHTML =
+ fixtureSetup(
'
Some paragraph with text ' +
' link ' +
' Some paragraph with text ' +
- '
';
+ '');
var link = document.getElementById('link');
assert.isFalse(axe.commons.dom.isInTextBlock(link));
});
it('treats hr elements the same as br elements', function () {
- fixture.innerHTML =
- '
Some paragraph with text
' +
+ fixtureSetup(
+ '
Some paragraph with text ' +
' link' +
' Some paragraph with text ' +
- '';
+ '
');
var link = document.getElementById('link');
assert.isFalse(axe.commons.dom.isInTextBlock(link));
});
it('ignore comments', function () {
- fixture.innerHTML =
+ fixtureSetup(
'
';
+ fixtureSetup(div);
+
+ var link = shadow.querySelector('#link');
+ assert.isFalse(axe.commons.dom.isInTextBlock(link));
+ });
+
});
\ No newline at end of file
From 7ea8d6b82b76e688ac3384c37a5d13841798138d Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Tue, 11 Jul 2017 17:58:08 +0200
Subject: [PATCH 088/142] fix: Pass all tests that use accessibleText
---
test/checks/keyboard/focusable-no-name.js | 4 ++--
test/testutils.js | 1 -
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/test/checks/keyboard/focusable-no-name.js b/test/checks/keyboard/focusable-no-name.js
index 70191cc18b..4ed49328c0 100644
--- a/test/checks/keyboard/focusable-no-name.js
+++ b/test/checks/keyboard/focusable-no-name.js
@@ -27,13 +27,13 @@ describe('focusable-no-name', function () {
assert.isTrue(checks['focusable-no-name'].evaluate(node));
});
- it('should fail if element is tabable with no name - ARIA', function () {
+ it('should fail if element is tabbable with no name - ARIA', function () {
fixtureSetup('');
var node = fixture.querySelector('span');
assert.isTrue(checks['focusable-no-name'].evaluate(node));
});
- it('should pass if the element is tabable but has an accessible name', function () {
+ it('should pass if the element is tabbable but has an accessible name', function () {
fixtureSetup('');
var node = fixture.querySelector('a');
assert.isFalse(checks['focusable-no-name'].evaluate(node));
diff --git a/test/testutils.js b/test/testutils.js
index ab8cfc811c..e3b13ec8ac 100644
--- a/test/testutils.js
+++ b/test/testutils.js
@@ -27,7 +27,6 @@ testUtils.fixtureSetup = function (content) {
axe._tree = axe.utils.getFlattenedTree(fixture);
return fixture;
};
-
/**
* Create check arguments
*
From f729e25300bc82a2679870e653b13f4c3aeee31a Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Thu, 13 Jul 2017 16:31:40 -0700
Subject: [PATCH 089/142] feat: add shadow support to aria-required-children
Closes https://github.com/dequelabs/axe-core/issues/421
---
lib/checks/aria/required-children.js | 12 +--
test/checks/aria/required-children.js | 129 +++++++++++++++++---------
2 files changed, 93 insertions(+), 48 deletions(-)
diff --git a/lib/checks/aria/required-children.js b/lib/checks/aria/required-children.js
index 6d61ef5db5..73e30dd1e3 100644
--- a/lib/checks/aria/required-children.js
+++ b/lib/checks/aria/required-children.js
@@ -3,7 +3,7 @@ implicitNodes = axe.commons.aria.implicitNodes,
matchesSelector = axe.commons.utils.matchesSelector,
idrefs = axe.commons.dom.idrefs;
-function owns(node, role, ariaOwned) {
+function owns(node, virtualTree, role, ariaOwned) {
if (node === null) { return false; }
var implicit = implicitNodes(role),
selector = ['[role="' + role + '"]'];
@@ -13,9 +13,8 @@ function owns(node, role, ariaOwned) {
}
selector = selector.join(',');
-
- return ariaOwned ? (matchesSelector(node, selector) || !!node.querySelector(selector)) :
- !!node.querySelector(selector);
+ return ariaOwned ? (matchesSelector(node, selector) || !!axe.utils.querySelectorAll(virtualTree, selector)[0]) :
+ !!axe.utils.querySelectorAll(virtualTree, selector)[0];
}
function ariaOwns(nodes, role) {
@@ -23,7 +22,8 @@ function ariaOwns(nodes, role) {
for (index = 0, length = nodes.length; index < length; index++) {
if (nodes[index] === null) { continue; }
- if (owns(nodes[index], role, true)) {
+ let virtualTree = axe.utils.getFlattenedTree(nodes[index]);
+ if (owns(nodes[index], virtualTree, role, true)) {
return true;
}
}
@@ -39,7 +39,7 @@ function missingRequiredChildren(node, childRoles, all) {
for (i = 0; i < l; i++) {
var r = childRoles[i];
- if (owns(node, r) || ariaOwns(ownedElements, r)) {
+ if (owns(node, virtualNode, r) || ariaOwns(ownedElements, r)) {
if (!all) { return null; }
} else {
if (all) { missing.push(r); }
diff --git a/test/checks/aria/required-children.js b/test/checks/aria/required-children.js
index 57cdb5046c..46296572c4 100644
--- a/test/checks/aria/required-children.js
+++ b/test/checks/aria/required-children.js
@@ -2,6 +2,7 @@ describe('aria-required-children', function () {
'use strict';
var fixture = document.getElementById('fixture');
+ var shadowSupported = axe.testUtils.shadowSupport.v1;
var checkContext = {
_data: null,
@@ -10,97 +11,141 @@ describe('aria-required-children', function () {
}
};
+ function checkSetup (html, options, target) {
+ fixture.innerHTML = html;
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var node = fixture.querySelector(target || '#target');
+ var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node);
+ return [node, options, virtualNode];
+ }
+
afterEach(function () {
fixture.innerHTML = '';
+ axe._tree = undefined;
checkContext._data = null;
});
it('should detect missing sole required child', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+
+ assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
+ assert.deepEqual(checkContext._data, ['listitem']);
+ });
+
+ (shadowSupported ? it : xit)
+ ('should detect missing sole required child in shadow tree', function () {
+ fixture.innerHTML = '';
+
+ var target = document.querySelector('#target');
+ var shadowRoot = target.attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '
Nothing here.
';
+
+ var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
+ var virtualTarget = axe.utils.getNodeFromTree(tree[0], target);
+
+ var params = [target, undefined, virtualTarget];
+ assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['listitem']);
});
it('should detect multiple missing required children when one required', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+
+ assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
+ assert.deepEqual(checkContext._data, ['rowgroup', 'row']);
+ });
+
+ (shadowSupported ? it : xit)
+ ('should detect missing multiple required children in shadow tree when one required', function () {
+ fixture.innerHTML = '';
+
+ var target = document.querySelector('#target');
+ var shadowRoot = target.attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '
Nothing here.
';
+
+ var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
+ var virtualTarget = axe.utils.getNodeFromTree(tree[0], target);
+
+ var params = [target, undefined, virtualTarget];
+ assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['rowgroup', 'row']);
});
it('should detect multiple missing required children when all required', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['listbox', 'textbox']);
});
it('should detect single missing required child when all required', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['textbox']);
});
it('should pass all existing required children when all required', function () {
- fixture.innerHTML = '
Nothing here.
Textbox
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
Textbox
');
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
+ });
+
+ (shadowSupported ? it : xit)
+ ('should pass all existing required children in shadow tree when all required', function () {
+ fixture.innerHTML = '';
+
+ var target = document.querySelector('#target');
+ var shadowRoot = target.attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '
Nothing here.
Textbox
';
+
+ var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
+ var virtualTarget = axe.utils.getNodeFromTree(tree[0], target);
+
+ var params = [target, undefined, virtualTarget];
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
it('should pass one indirectly aria-owned child when one required', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
it('should not break if aria-owns points to non-existent node', function () {
- fixture.innerHTML = '';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('');
+ assert.isFalse(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
it('should pass one existing aria-owned child when one required', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
it('should pass one existing required child when one required', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
it('should pass one existing required child when one required because of implicit role', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
it('should pass when a child with an implicit role is present', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
it('should pass direct existing required children', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Just a regular ol p that contains a...
Nothing here.
');
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
it('should return true when a role has no required owned', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
});
From c62a6d350c31f57580088b3977623f5049ab8bf0 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Fri, 14 Jul 2017 12:44:38 +0200
Subject: [PATCH 090/142] test: More testing for accessibleText()
# Conflicts:
# lib/commons/dom/find-elms-in-context.js
# lib/commons/text/accessible-text.js
# test/checks/keyboard/focusable-no-name.js
# test/checks/tables/same-caption-summary.js
# test/commons/text/accessible-text.js
---
test/checks/keyboard/focusable-no-name.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/test/checks/keyboard/focusable-no-name.js b/test/checks/keyboard/focusable-no-name.js
index 4ed49328c0..70191cc18b 100644
--- a/test/checks/keyboard/focusable-no-name.js
+++ b/test/checks/keyboard/focusable-no-name.js
@@ -27,13 +27,13 @@ describe('focusable-no-name', function () {
assert.isTrue(checks['focusable-no-name'].evaluate(node));
});
- it('should fail if element is tabbable with no name - ARIA', function () {
+ it('should fail if element is tabable with no name - ARIA', function () {
fixtureSetup('');
var node = fixture.querySelector('span');
assert.isTrue(checks['focusable-no-name'].evaluate(node));
});
- it('should pass if the element is tabbable but has an accessible name', function () {
+ it('should pass if the element is tabable but has an accessible name', function () {
fixtureSetup('');
var node = fixture.querySelector('a');
assert.isFalse(checks['focusable-no-name'].evaluate(node));
From 71a97ac7ac4744a2b3244aca02256b0310bd8c40 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Fri, 14 Jul 2017 09:13:24 -0700
Subject: [PATCH 091/142] test: use abstracted checkSetup from testutils
---
test/checks/aria/required-children.js | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/test/checks/aria/required-children.js b/test/checks/aria/required-children.js
index 46296572c4..e63ff498b3 100644
--- a/test/checks/aria/required-children.js
+++ b/test/checks/aria/required-children.js
@@ -11,13 +11,7 @@ describe('aria-required-children', function () {
}
};
- function checkSetup (html, options, target) {
- fixture.innerHTML = html;
- axe._tree = axe.utils.getFlattenedTree(fixture);
- var node = fixture.querySelector(target || '#target');
- var virtualNode = axe.utils.getNodeFromTree(axe._tree[0], node);
- return [node, options, virtualNode];
- }
+ var checkSetup = axe.testUtils.checkSetup;
afterEach(function () {
fixture.innerHTML = '';
From 9bf2870e4ccb954238786f75b191860922411966 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Fri, 14 Jul 2017 09:14:00 -0700
Subject: [PATCH 092/142] fix: get virtualNode with getNodeFromTree
---
lib/checks/aria/required-children.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/checks/aria/required-children.js b/lib/checks/aria/required-children.js
index 73e30dd1e3..19bf438f53 100644
--- a/lib/checks/aria/required-children.js
+++ b/lib/checks/aria/required-children.js
@@ -22,7 +22,7 @@ function ariaOwns(nodes, role) {
for (index = 0, length = nodes.length; index < length; index++) {
if (nodes[index] === null) { continue; }
- let virtualTree = axe.utils.getFlattenedTree(nodes[index]);
+ let virtualTree = axe.utils.getNodeFromTree(axe._tree[0], nodes[index]);
if (owns(nodes[index], virtualTree, role, true)) {
return true;
}
From 6ed29f00e4b85bbca3f611f1777187312661bea2 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Tue, 18 Jul 2017 13:58:35 -0700
Subject: [PATCH 093/142] feat(aria-required-parent): add Shadow DOM support
Closes https://github.com/dequelabs/axe-core/issues/422
---
lib/checks/aria/required-parent.js | 3 +-
test/checks/aria/required-parent.js | 85 ++++++++++++++++++++++-------
2 files changed, 68 insertions(+), 20 deletions(-)
diff --git a/lib/checks/aria/required-parent.js b/lib/checks/aria/required-parent.js
index d227f0635d..5a4ef3614f 100644
--- a/lib/checks/aria/required-parent.js
+++ b/lib/checks/aria/required-parent.js
@@ -36,7 +36,8 @@ function getAriaOwners(element) {
while (element) {
if (element.getAttribute('id')) {
const id = axe.commons.utils.escapeSelector(element.getAttribute('id'));
- o = document.querySelector(`[aria-owns~=${id}]`);
+ let doc = axe.commons.dom.getRootNode(element);
+ o = doc.querySelector(`[aria-owns~=${id}]`);
if (o) { owners.push(o); }
}
element = element.parentElement;
diff --git a/test/checks/aria/required-parent.js b/test/checks/aria/required-parent.js
index aef8f598ae..e105231c9f 100644
--- a/test/checks/aria/required-parent.js
+++ b/test/checks/aria/required-parent.js
@@ -2,6 +2,7 @@ describe('aria-required-parent', function () {
'use strict';
var fixture = document.getElementById('fixture');
+ var shadowSupported = axe.testUtils.shadowSupport.v1;
var checkContext = {
_data: null,
@@ -10,47 +11,93 @@ describe('aria-required-parent', function () {
}
};
+ var checkSetup = axe.testUtils.checkSetup;
+
afterEach(function () {
fixture.innerHTML = '';
checkContext._data = null;
});
it('should detect missing required parent', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks['aria-required-parent'].evaluate.call(checkContext, node));
+ var params = checkSetup('
';
+
+ var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
+ var shadowContent = shadowRoot.querySelector('#target');
+ var virtualTarget = axe.utils.getNodeFromTree(tree[0], shadowContent);
+
+ var params = [shadowContent, undefined, virtualTarget];
+ assert.isFalse(checks['aria-required-parent'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['list']);
});
it('should pass when required parent is present in an ancestral aria-owns context', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-parent'].evaluate.call(checkContext, node));
+ var snippet = '
'+
+ '
Nothing here.
';
+ var params = checkSetup(snippet);
+ assert.isTrue(checks['aria-required-parent'].evaluate.apply(checkContext, params));
});
it('should fail when wrong role is present in an aria-owns context', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isFalse(checks['aria-required-parent'].evaluate.call(checkContext, node));
+ var params = checkSetup(
+ '
' +
+ '
Nothing here.
'
+ );
+ assert.isFalse(checks['aria-required-parent'].evaluate.apply(checkContext, params));
assert.deepEqual(checkContext._data, ['list']);
});
-
it('should pass when required parent is present in an aria-owns context', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-parent'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-parent'].evaluate.apply(checkContext, params));
});
it('should pass when at least one required parent of multiple is present', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-parent'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-parent'].evaluate.apply(checkContext, params));
});
+
it('should pass when required parent is present', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-parent'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-parent'].evaluate.apply(checkContext, params));
});
+ (shadowSupported ? it : xit)
+ ('should pass when required parent is present across shadow boundary', function () {
+ fixture.innerHTML = '';
+
+ var shadowRoot = document.querySelector('#parent').attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '
Nothing here.
';
+
+ var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
+ var shadowContent = shadowRoot.querySelector('#target');
+ var virtualTarget = axe.utils.getNodeFromTree(tree[0], shadowContent);
+
+ var params = [shadowContent, undefined, virtualTarget];
+ assert.isTrue(checks['aria-required-parent'].evaluate.apply(checkContext, params));
+ });
+
+ (shadowSupported ? it : xit)
+ ('should fail when aria-owns context crosses shadow boundary', function () {
+ fixture.innerHTML = '
';
+
+ var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
+ var shadowContent = shadowRoot.querySelector('#target');
+ var virtualTarget = axe.utils.getNodeFromTree(tree[0], shadowContent);
+
+ var params = [shadowContent, undefined, virtualTarget];
+ assert.isFalse(checks['aria-required-parent'].evaluate.apply(checkContext, params));
+ });
});
From 08509b936305e61bf9b376add504a1b13e748b8b Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Thu, 20 Jul 2017 11:14:56 +0200
Subject: [PATCH 094/142] chore(region): Minor test/comment fixes
---
lib/checks/navigation/region.js | 24 +++++++++++++++---------
test/checks/navigation/region.js | 10 +++++++++-
test/commons/dom/has-content.js | 2 +-
3 files changed, 25 insertions(+), 11 deletions(-)
diff --git a/lib/checks/navigation/region.js b/lib/checks/navigation/region.js
index 7e61f79e3a..a956251dd2 100644
--- a/lib/checks/navigation/region.js
+++ b/lib/checks/navigation/region.js
@@ -1,27 +1,33 @@
const { dom, aria } = axe.commons;
+
+// Return the skplink, if any
function getSkiplink (virtualNode) {
- const firstLink = axe.utils.querySelectorAll(virtualNode, 'a[href]')[0];
- if (firstLink && axe.commons.dom.getElementByReference(firstLink.actualNode, 'href')) {
- return firstLink.actualNode;
- }
+ const firstLink = axe.utils.querySelectorAll(virtualNode, 'a[href]')[0];
+ if (firstLink && axe.commons.dom.getElementByReference(firstLink.actualNode, 'href')) {
+ return firstLink.actualNode;
+ }
}
const skipLink = getSkiplink(virtualNode);
const landmarkRoles = aria.getRolesByType('landmark');
+
+// Create a list of nodeNames that have a landmark as an implicit role
const implicitLandmarks = landmarkRoles
.reduce((arr, role) => arr.concat(aria.implicitNodes(role)), [])
.filter(r => r !== null).map(r => r.toUpperCase());
-// Check if the current element it the skiplink
+// Check if the current element is the skiplink
function isSkipLink (node) {
return skipLink && skipLink === node;
}
// Check if the current element is a landmark
function isLandmark (node) {
- const nodeName = node.nodeName.toUpperCase();
- return (landmarkRoles.includes(node.getAttribute('role')) ||
- implicitLandmarks.includes(nodeName));
+ if (node.hasAttribute('role')) {
+ return landmarkRoles.includes(node.getAttribute('role').toLowerCase());
+ } else {
+ return implicitLandmarks.includes(node.nodeName.toUpperCase());
+ }
}
/**
@@ -29,7 +35,7 @@ function isLandmark (node) {
*/
function findRegionlessElms (virtualNode) {
const node = virtualNode.actualNode;
- // End recursion if the element a landmark, skiplink, or hidden content
+ // End recursion if the element is a landmark, skiplink, or hidden content
if (isLandmark(node) || isSkipLink(node) || !dom.isVisible(node, true)) {
return [];
diff --git a/test/checks/navigation/region.js b/test/checks/navigation/region.js
index 5333f22c8f..7a2c35328e 100644
--- a/test/checks/navigation/region.js
+++ b/test/checks/navigation/region.js
@@ -79,7 +79,7 @@ describe('region', function () {
});
it('should considered aria labelled elements as content', function () {
- var checkArgs = checkSetup('
Content
');
+ var checkArgs = checkSetup('
Content
');
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.deepEqual(checkContext._relatedNodes, [
@@ -94,6 +94,14 @@ describe('region', function () {
assert.lengthOf(checkContext._relatedNodes, 0);
});
+ it('ignores native landmark elements with an overwriting role', function () {
+ var checkArgs = checkSetup('
');
+
+ assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.lengthOf(checkContext._relatedNodes, 1);
+ assert.deepEqual(checkContext._relatedNodes, [fixture.querySelector('main')]);
+ });
+
(shadowSupport.v1 ? it : xit)('should test Shadow tree content', function () {
var div = document.createElement('div');
var shadow = div.attachShadow({ mode: 'open' });
diff --git a/test/commons/dom/has-content.js b/test/commons/dom/has-content.js
index a0b8b87144..68c6c6c0a4 100644
--- a/test/commons/dom/has-content.js
+++ b/test/commons/dom/has-content.js
@@ -77,7 +77,7 @@ describe('dom.hasContent', function () {
);
});
- it('is false if noRecurstion is true and the content is not in a child', function () {
+ it('is false if noRecursion is true and the content is not in a child', function () {
fixture.innerHTML = '
text
';
tree = axe.utils.getFlattenedTree(fixture);
From 4874327401e96ef553e96ff69767539e693fac38 Mon Sep 17 00:00:00 2001
From: Dylan Barrell
Date: Fri, 21 Jul 2017 04:48:07 -0400
Subject: [PATCH 095/142] fix: get tests all passing (#457)
---
lib/core/base/audit.js | 2 +-
lib/core/utils/flattened-tree.js | 3 +++
test/checks/navigation/region.js | 2 +-
test/commons/dom/is-in-text-block.js | 2 +-
test/core/utils/flattened-tree.js | 6 ++++++
5 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/lib/core/base/audit.js b/lib/core/base/audit.js
index d9f80598f8..c07b66818f 100644
--- a/lib/core/base/audit.js
+++ b/lib/core/base/audit.js
@@ -144,7 +144,7 @@ Audit.prototype.run = function (context, options, resolve, reject) {
'use strict';
this.validateOptions(options);
- axe._tree = axe.utils.getFlattenedTree(document.body); //cache the flattened tree
+ axe._tree = axe.utils.getFlattenedTree(document.documentElement); //cache the flattened tree
var q = axe.utils.queue();
this.rules.forEach(function (rule) {
if (axe.utils.ruleShouldRun(rule, context, options)) {
diff --git a/lib/core/utils/flattened-tree.js b/lib/core/utils/flattened-tree.js
index 8d7aabb38a..2fc6b192a6 100644
--- a/lib/core/utils/flattened-tree.js
+++ b/lib/core/utils/flattened-tree.js
@@ -123,6 +123,9 @@ axe.utils.getFlattenedTree = function (node, shadowId) {
axe.utils.getNodeFromTree = function (vNode, node) {
var found;
+ if (vNode.actualNode === node) {
+ return vNode;
+ }
vNode.children.forEach((candidate) => {
var retVal;
diff --git a/test/checks/navigation/region.js b/test/checks/navigation/region.js
index 7a2c35328e..4ce4aa5d1c 100644
--- a/test/checks/navigation/region.js
+++ b/test/checks/navigation/region.js
@@ -95,7 +95,7 @@ describe('region', function () {
});
it('ignores native landmark elements with an overwriting role', function () {
- var checkArgs = checkSetup('
');
+ var checkArgs = checkSetup('
Content
');
assert.isFalse(checks.region.evaluate.apply(checkContext, checkArgs));
assert.lengthOf(checkContext._relatedNodes, 1);
diff --git a/test/commons/dom/is-in-text-block.js b/test/commons/dom/is-in-text-block.js
index f89519bb19..20574d0ba6 100644
--- a/test/commons/dom/is-in-text-block.js
+++ b/test/commons/dom/is-in-text-block.js
@@ -143,7 +143,7 @@ describe('dom.isInTextBlock', function () {
assert.isFalse(axe.commons.dom.isInTextBlock(link));
});
- (shadowSupport ? it : xit)('can reach outside a shadow tree', function () {
+ (shadowSupport.v1 ? it : xit)('can reach outside a shadow tree', function () {
var div = document.createElement('div');
div.innerHTML = 'Some paragraph with text ';
var shadow = div.querySelector('span').attachShadow({ mode: 'open' });
diff --git a/test/core/utils/flattened-tree.js b/test/core/utils/flattened-tree.js
index f8121c219e..279a4af8a6 100644
--- a/test/core/utils/flattened-tree.js
+++ b/test/core/utils/flattened-tree.js
@@ -231,6 +231,12 @@ if (shadowSupport.v1) {
assert.equal(node, vNode.actualNode);
assert.equal(vNode.actualNode.textContent, '1');
});
+ it('should find the virtual node if it is the very top of the tree', function () {
+ var virtualDOM = axe.utils.getFlattenedTree(fixture);
+ var vNode = axe.utils.getNodeFromTree(virtualDOM[0], virtualDOM[0].actualNode);
+ assert.isDefined(vNode);
+ assert.equal(virtualDOM[0].actualNode, vNode.actualNode);
+ });
});
}
From e2a964207729d7c712819d67e0ed27aa688179b6 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Thu, 20 Jul 2017 13:07:29 -0700
Subject: [PATCH 096/142] feat: add shadow support to group-labelledby
Closes https://github.com/dequelabs/axe-core/issues/426
---
lib/checks/forms/labelledby.js | 7 +++---
test/checks/forms/labelledby.js | 38 +++++++++++++++++++++++++++++++++
2 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/lib/checks/forms/labelledby.js b/lib/checks/forms/labelledby.js
index a06ddd7ac4..a9b3e84263 100644
--- a/lib/checks/forms/labelledby.js
+++ b/lib/checks/forms/labelledby.js
@@ -3,7 +3,8 @@ this.data({
type: node.getAttribute('type')
});
-var matchingNodes = document.querySelectorAll('input[type="' +
+var doc = axe.commons.dom.getRootNode(node);
+var matchingNodes = doc.querySelectorAll('input[type="' +
axe.commons.utils.escapeSelector(node.type) + '"][name="' + axe.commons.utils.escapeSelector(node.name) + '"]');
if (matchingNodes.length <= 1) {
return true;
@@ -15,9 +16,9 @@ return [].map.call(matchingNodes, function (m) {
return l ? l.split(/\s+/) : [];
}).reduce(function (prev, curr) {
return prev.filter(function (n) {
- return curr.indexOf(n) !== -1;
+ return curr.includes(n);
});
}).filter(function (n) {
- var labelNode = document.getElementById(n);
+ var labelNode = doc.getElementById(n);
return labelNode && axe.commons.text.accessibleText(labelNode);
}).length !== 0;
diff --git a/test/checks/forms/labelledby.js b/test/checks/forms/labelledby.js
index 6525fa4b61..0c5c16f83b 100644
--- a/test/checks/forms/labelledby.js
+++ b/test/checks/forms/labelledby.js
@@ -3,6 +3,8 @@ describe('group-labelledby', function () {
var fixture = document.getElementById('fixture');
var fixtureSetup = axe.testUtils.fixtureSetup;
+ var shadowSupport = axe.testUtils.shadowSupport.v1;
+
var checkContext = {
_data: null,
data: function (d) {
@@ -10,6 +12,10 @@ describe('group-labelledby', function () {
}
};
+ beforeEach(function () {
+ axe._tree = undefined;
+ });
+
afterEach(function () {
fixture.innerHTML = '';
checkContext._data = null;
@@ -114,6 +120,38 @@ describe('group-labelledby', function () {
});
});
+ (shadowSupport ? it : xit)
+ ('should return false if label is outside of shadow boundary', function () {
+ fixture.innerHTML = '
Label
';
+ var shadowRoot = fixture.querySelector('#container').attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '' +
+ '' +
+ '';
+
+ var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
+ var shadowContent = shadowRoot.querySelector('#target');
+ var virtualTarget = axe.utils.getNodeFromTree(tree[0], shadowContent);
+
+ var params = [shadowContent, undefined, virtualTarget];
+ assert.isFalse(check.evaluate.apply(checkContext, params));
+ });
+
+ (shadowSupport ? it : xit)
+ ('should return true if all ' + type + ' components are in the shadow boundary', function () {
+ fixture.innerHTML = '';
+
+ var shadowRoot = fixture.querySelector('#container').attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '
Label
' +
+ '' +
+ '';
+
+ var tree = axe._tree = axe.utils.getFlattenedTree(fixture);
+ var shadowContent = shadowRoot.querySelector('#target');
+ var virtualTarget = axe.utils.getNodeFromTree(tree[0], shadowContent);
+
+ var params = [shadowContent, undefined, virtualTarget];
+ assert.isTrue(check.evaluate.apply(checkContext, params));
+ });
};
}
From da148d344c33c8c4dc2300cbb041cd84aa9f2efe Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Tue, 18 Jul 2017 12:29:19 +0200
Subject: [PATCH 097/142] feat: fieldset check shadow DOM
---
lib/checks/forms/fieldset.js | 19 ++++++++++++------
test/checks/forms/fieldset.js | 36 ++++++++++++++++++++++++++++++++---
2 files changed, 46 insertions(+), 9 deletions(-)
diff --git a/lib/checks/forms/fieldset.js b/lib/checks/forms/fieldset.js
index 8800412c91..4eaaedfb12 100644
--- a/lib/checks/forms/fieldset.js
+++ b/lib/checks/forms/fieldset.js
@@ -57,21 +57,28 @@ function spliceCurrentNode(nodes, current) {
}
function runCheck(element) {
- var name = axe.commons.utils.escapeSelector(node.name);
- var matchingNodes = document.querySelectorAll('input[type="' +
+ const name = axe.commons.utils.escapeSelector(node.name);
+ const root = axe.commons.dom.getRootNode(node);
+ const matchingNodes = root.querySelectorAll('input[type="' +
axe.commons.utils.escapeSelector(node.type) + '"][name="' + name + '"]');
+
if (matchingNodes.length < 2) {
return true;
}
- var fieldset = axe.commons.dom.findUp(element, 'fieldset');
- var group = axe.commons.dom.findUp(element, '[role="group"]' + (node.type === 'radio' ? ',[role="radiogroup"]' : ''));
+ const fieldset = axe.commons.dom.findUp(element, 'fieldset');
+ const group = axe.commons.dom.findUp(element, '[role="group"]' +
+ (node.type === 'radio' ? ',[role="radiogroup"]' : ''));
+
if (!group && !fieldset) {
failureCode = 'no-group';
self.relatedNodes(spliceCurrentNode(matchingNodes, element));
return false;
- }
- return fieldset ? checkFieldset(fieldset, name) : checkARIAGroup(group, name);
+ } else if (fieldset) {
+ return checkFieldset(fieldset, name);
+ } else {
+ return checkARIAGroup(group, name);
+ }
}
var data = {
diff --git a/test/checks/forms/fieldset.js b/test/checks/forms/fieldset.js
index 2a6e3ee4ac..8b8cb2844c 100644
--- a/test/checks/forms/fieldset.js
+++ b/test/checks/forms/fieldset.js
@@ -1,7 +1,9 @@
describe('fieldset', function () {
'use strict';
var fixture = document.getElementById('fixture');
+ var shadowSupport = axe.testUtils.shadowSupport;
var fixtureSetup = axe.testUtils.fixtureSetup;
+
var checkContext = {
_data: null,
data: function (d) {
@@ -158,7 +160,6 @@ describe('fieldset', function () {
});
});
-
it('should return true if a properly labelled-by ARIA group contains only the right elements - special characters', function () {
fixtureSetup('
Label
' +
'
' +
@@ -182,7 +183,6 @@ describe('fieldset', function () {
assert.isTrue(checks.fieldset.evaluate.call(checkContext, node));
});
-
it('should ignore hidden inputs', function () {
fixtureSetup('');
var node = fixture.querySelector('#target');
assert.isTrue(checks.fieldset.evaluate.call(checkContext, node));
-
});
it('should allow elements to be contained in 2 or more fieldsets', function () {
@@ -218,6 +217,36 @@ describe('fieldset', function () {
var node = fixture.querySelector('#target');
assert.isTrue(checks.fieldset.evaluate.call(checkContext, node));
});
+
+ (shadowSupport ? it : xit)('should find fieldsets outside a shadow tree', function () {
+ var fieldset = document.createElement('fieldset');
+ fieldset.innerHTML = ' ';
+
+ var div = fieldset.querySelector('div');
+ var shadow = div.attachShadow({ mode: 'open' });
+ shadow.innerHTML =
+ '' +
+ '';
+ fixtureSetup(fieldset);
+
+ var node = shadow.querySelector('#target');
+ assert.isTrue(checks.fieldset.evaluate.call(checkContext, node));
+ });
+
+ (shadowSupport ? it : xit)('should find fieldsets inside a shadow tree', function () {
+ var div = document.createElement('div');
+ div.innerHTML =
+ '' +
+ '';
+
+ var shadow = div.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '';
+ fixtureSetup(div);
+
+ var node = div.querySelector('#target');
+ assert.isTrue(checks.fieldset.evaluate.call(checkContext, node));
+ });
}
@@ -249,4 +278,5 @@ describe('fieldset', function () {
assert.isFalse(checks.fieldset.evaluate.call(checkContext, node));
});
});
+
});
From 9df8c9c2a9ca23b5ad08dfb98fd50a2010d441e2 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Fri, 21 Jul 2017 10:33:06 -0700
Subject: [PATCH 098/142] test: add missing .v1 property to fieldset
---
test/checks/forms/fieldset.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/checks/forms/fieldset.js b/test/checks/forms/fieldset.js
index 8b8cb2844c..35fb506153 100644
--- a/test/checks/forms/fieldset.js
+++ b/test/checks/forms/fieldset.js
@@ -1,7 +1,7 @@
describe('fieldset', function () {
'use strict';
var fixture = document.getElementById('fixture');
- var shadowSupport = axe.testUtils.shadowSupport;
+ var shadowSupport = axe.testUtils.shadowSupport.v1;
var fixtureSetup = axe.testUtils.fixtureSetup;
var checkContext = {
From 93a0273b43113a247cd16a4115e33afa08bd8f70 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Mon, 31 Jul 2017 05:03:48 -0400
Subject: [PATCH 099/142] chore: add descriptions to Grunt tasks (#459)
Closes https://github.com/dequelabs/axe-core/issues/364
---
build/tasks/add-locale.js | 4 +++-
build/tasks/configure.js | 5 +++--
build/tasks/fixture.js | 4 +++-
build/tasks/langs.js | 4 +++-
build/tasks/test-webdriver.js | 4 +++-
build/tasks/testconfig.js | 2 +-
build/tasks/update-help.js | 4 +++-
build/tasks/validate.js | 4 +++-
8 files changed, 22 insertions(+), 9 deletions(-)
diff --git a/build/tasks/add-locale.js b/build/tasks/add-locale.js
index a7ec59f3f1..50b17a0b61 100644
--- a/build/tasks/add-locale.js
+++ b/build/tasks/add-locale.js
@@ -3,7 +3,9 @@
var buildManual = require('../build-manual');
module.exports = function (grunt) {
- grunt.registerMultiTask('add-locale', function () {
+ grunt.registerMultiTask('add-locale',
+ 'Task for localizing messages in rules and checks',
+ function () {
var options = this.options({
rules: ['lib/rules/**/*.json'],
checks: ['lib/checks/**/*.json'],
diff --git a/build/tasks/configure.js b/build/tasks/configure.js
index a15fe8064f..00b4b39b94 100644
--- a/build/tasks/configure.js
+++ b/build/tasks/configure.js
@@ -2,8 +2,9 @@
'use strict';
var buildRules = require('../configure');
module.exports = function (grunt) {
- grunt.registerMultiTask('configure', function () {
-
+ grunt.registerMultiTask('configure',
+ 'Task for configuring rules and checks',
+ function () {
var done = this.async();
var options = this.options({
rules: ['lib/rules/**/*.json'],
diff --git a/build/tasks/fixture.js b/build/tasks/fixture.js
index e58f1d5d9a..45303d202e 100644
--- a/build/tasks/fixture.js
+++ b/build/tasks/fixture.js
@@ -2,7 +2,9 @@
'use strict';
module.exports = function (grunt) {
- grunt.registerMultiTask('fixture', function () {
+ grunt.registerMultiTask('fixture',
+ 'Task for generating HTML test fixtures from a common template',
+ function () {
var options = this.options({
fixture: 'test/runner.tmpl',
testCwd: 'test/core',
diff --git a/build/tasks/langs.js b/build/tasks/langs.js
index a99203d8ed..d5f489710e 100644
--- a/build/tasks/langs.js
+++ b/build/tasks/langs.js
@@ -47,7 +47,9 @@ module.exports = function (grunt) {
].join('');
grunt.file.write(path, template);
}
- grunt.registerMultiTask('langs', function () {
+ grunt.registerMultiTask('langs',
+ 'Task for generating commons language codes from IANA registry',
+ function () {
var done = this.async();
var ianaLangsURL = 'http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry';
if (!this.data.check) {
diff --git a/build/tasks/test-webdriver.js b/build/tasks/test-webdriver.js
index 53c730ae1b..3c23fbf814 100644
--- a/build/tasks/test-webdriver.js
+++ b/build/tasks/test-webdriver.js
@@ -117,7 +117,9 @@ module.exports = function (grunt) {
/**
* Run all tests in a browser using webdriver
*/
- grunt.registerMultiTask('test-webdriver', function () {
+ grunt.registerMultiTask('test-webdriver',
+ 'Task for launching Webdriver with options and running tests against options URLs',
+ function () {
var driver;
var done = this.async();
var options = this.options({
diff --git a/build/tasks/testconfig.js b/build/tasks/testconfig.js
index 306cfb2086..87db90bf23 100644
--- a/build/tasks/testconfig.js
+++ b/build/tasks/testconfig.js
@@ -3,7 +3,7 @@
module.exports = function (grunt) {
grunt.registerMultiTask('testconfig',
- 'This task creates a file with all the source test config and HTML fixutres in a single JS object "test"',
+ 'This task creates a file with all the source test config and HTML fixtures in a single JS object `tests`',
function () {
var result = {
diff --git a/build/tasks/update-help.js b/build/tasks/update-help.js
index 3d2edf58a5..ff8d8e99cb 100644
--- a/build/tasks/update-help.js
+++ b/build/tasks/update-help.js
@@ -2,7 +2,9 @@
'use strict';
module.exports = function (grunt) {
- grunt.registerMultiTask('update-help', function () {
+ grunt.registerMultiTask('update-help',
+ 'Task for updating Deque University helpUrls based on rule JSON files',
+ function () {
var options = this.options({
version: '1.0.0'
});
diff --git a/build/tasks/validate.js b/build/tasks/validate.js
index 04f4d8d270..ed86863df4 100644
--- a/build/tasks/validate.js
+++ b/build/tasks/validate.js
@@ -233,7 +233,9 @@ function validateFiles(grunt, files, schema) {
}
module.exports = function (grunt) {
- grunt.registerMultiTask('validate', function () {
+ grunt.registerMultiTask('validate',
+ 'Task for validating API schema for tools, checks and rules',
+ function () {
var schemas = createSchemas();
var options = this.options();
if (!options.type || !schemas[options.type]) {
From 439bc71674e8211e590c116c4c8ba15af4f255d2 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Wed, 19 Jul 2017 12:18:55 +0200
Subject: [PATCH 100/142] feat(duplicate-id): Add shadow DOM support
---
lib/checks/shared/duplicate-id.js | 24 ++++++-------
test/checks/shared/duplicate-id.js | 54 ++++++++++++++++++++++++++++--
2 files changed, 61 insertions(+), 17 deletions(-)
diff --git a/lib/checks/shared/duplicate-id.js b/lib/checks/shared/duplicate-id.js
index 70b5ae79b3..b7ef630c90 100644
--- a/lib/checks/shared/duplicate-id.js
+++ b/lib/checks/shared/duplicate-id.js
@@ -1,21 +1,17 @@
+const id = node.getAttribute('id').trim();
// Since empty ID's are not meaningful and are ignored by Edge, we'll
// let those pass.
-if (!node.getAttribute('id').trim()) {
+if (!id) {
return true;
}
+const root = axe.commons.dom.getRootNode(node);
+const matchingNodes = Array.from(root.querySelectorAll(
+ `[id="${ axe.commons.utils.escapeSelector(id) }"]`
+ )).filter(foundNode => foundNode !== node);
-const id = axe.commons.utils.escapeSelector(node.getAttribute('id'));
-var matchingNodes = document.querySelectorAll(`[id="${id}"]`);
-var related = [];
-
-for (var i = 0; i < matchingNodes.length; i++) {
- if (matchingNodes[i] !== node) {
- related.push(matchingNodes[i]);
- }
-}
-if (related.length) {
- this.relatedNodes(related);
+if (matchingNodes.length) {
+ this.relatedNodes(matchingNodes);
}
-this.data(node.getAttribute('id'));
+this.data(id);
-return (matchingNodes.length <= 1);
+return (matchingNodes.length === 0);
diff --git a/test/checks/shared/duplicate-id.js b/test/checks/shared/duplicate-id.js
index 93d8d41991..bb31ca4d54 100644
--- a/test/checks/shared/duplicate-id.js
+++ b/test/checks/shared/duplicate-id.js
@@ -2,7 +2,7 @@ describe('duplicate-id', function () {
'use strict';
var fixture = document.getElementById('fixture');
-
+ var shadowSupport = axe.testUtils.shadowSupport;
var checkContext = {
_relatedNodes: [],
@@ -27,7 +27,6 @@ describe('duplicate-id', function () {
assert.isTrue(checks['duplicate-id'].evaluate.call(checkContext, node));
assert.equal(checkContext._data, node.id);
assert.deepEqual(checkContext._relatedNodes, []);
-
});
it('should return false if there are multiple elements with an ID', function () {
@@ -39,7 +38,8 @@ describe('duplicate-id', function () {
});
it('should return remove duplicates', function () {
- assert.deepEqual(checks['duplicate-id'].after([{data: 'a'}, {data: 'b'}, {data: 'b'}]), [{data: 'a'}, {data: 'b'}]);
+ assert.deepEqual(checks['duplicate-id'].after([
+ {data: 'a'}, {data: 'b'}, {data: 'b'}]), [{data: 'a'}, {data: 'b'}]);
});
it('should ignore empty ids', function () {
@@ -58,4 +58,52 @@ describe('duplicate-id', function () {
assert.isTrue(checks['duplicate-id'].evaluate.call(checkContext, node));
});
+ (shadowSupport.v1 ? it : xit)('should find duplicate IDs in shadow trees', function () {
+ var div = document.createElement('div');
+ div.id = 'target';
+ var shadow = div.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '
text
';
+ var node = shadow.querySelector('span');
+ fixture.appendChild(div);
+
+ assert.isFalse(checks['duplicate-id'].evaluate.call(checkContext, node));
+ assert.lengthOf(checkContext._relatedNodes, 1);
+ assert.deepEqual(checkContext._relatedNodes, [shadow.querySelector('p')]);
+ });
+
+ (shadowSupport.v1 ? it : xit)('should ignore same IDs in shadow trees', function () {
+ var node = document.createElement('div');
+ node.id = 'target';
+ var shadow = node.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '';
+ fixture.appendChild(node);
+
+ assert.isTrue(checks['duplicate-id'].evaluate.call(checkContext, node));
+ assert.lengthOf(checkContext._relatedNodes, 0);
+ });
+
+ (shadowSupport.v1 ? it : xit)('should ignore same IDs outside shadow trees', function () {
+ var div = document.createElement('div');
+ div.id = 'target';
+ var shadow = div.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '';
+ var node = shadow.querySelector('#target');
+ fixture.appendChild(div);
+
+ assert.isTrue(checks['duplicate-id'].evaluate.call(checkContext, node));
+ assert.lengthOf(checkContext._relatedNodes, 0);
+ });
+
+ (shadowSupport.v1 ? it : xit)('should not ignore slotted elements', function () {
+ var node = document.createElement('div');
+ node.id = 'target';
+ node.innerHTML = '
text
';
+ var shadow = node.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '';
+ fixture.appendChild(node);
+
+ assert.isFalse(checks['duplicate-id'].evaluate.call(checkContext, node));
+ assert.lengthOf(checkContext._relatedNodes, 1);
+ assert.deepEqual(checkContext._relatedNodes, [node.querySelector('p')]);
+ });
});
From 797f9c28b164ab5a49c7b211ca8efb99d9a1e827 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Mon, 24 Jul 2017 15:24:50 +0200
Subject: [PATCH 101/142] test(duplicate-id): clarify test statement
---
test/checks/shared/duplicate-id.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/checks/shared/duplicate-id.js b/test/checks/shared/duplicate-id.js
index bb31ca4d54..2fd4c949a0 100644
--- a/test/checks/shared/duplicate-id.js
+++ b/test/checks/shared/duplicate-id.js
@@ -94,7 +94,7 @@ describe('duplicate-id', function () {
assert.lengthOf(checkContext._relatedNodes, 0);
});
- (shadowSupport.v1 ? it : xit)('should not ignore slotted elements', function () {
+ (shadowSupport.v1 ? it : xit)('should compare slotted content with the outer tree', function () {
var node = document.createElement('div');
node.id = 'target';
node.innerHTML = '
text
';
From 5b1cc7a0bec7d751fb55a7e386c10e1222c161e1 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Tue, 1 Aug 2017 12:47:57 +0200
Subject: [PATCH 102/142] test(duplicate-id): Update test text
---
test/checks/shared/duplicate-id.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/test/checks/shared/duplicate-id.js b/test/checks/shared/duplicate-id.js
index 2fd4c949a0..95e6627150 100644
--- a/test/checks/shared/duplicate-id.js
+++ b/test/checks/shared/duplicate-id.js
@@ -58,7 +58,7 @@ describe('duplicate-id', function () {
assert.isTrue(checks['duplicate-id'].evaluate.call(checkContext, node));
});
- (shadowSupport.v1 ? it : xit)('should find duplicate IDs in shadow trees', function () {
+ (shadowSupport.v1 ? it : xit)('should find duplicate IDs in the same shadow DOM', function () {
var div = document.createElement('div');
div.id = 'target';
var shadow = div.attachShadow({ mode: 'open' });
@@ -71,7 +71,7 @@ describe('duplicate-id', function () {
assert.deepEqual(checkContext._relatedNodes, [shadow.querySelector('p')]);
});
- (shadowSupport.v1 ? it : xit)('should ignore same IDs in shadow trees', function () {
+ (shadowSupport.v1 ? it : xit)('should ignore duplicate IDs if they are in different document roots', function () {
var node = document.createElement('div');
node.id = 'target';
var shadow = node.attachShadow({ mode: 'open' });
@@ -94,7 +94,7 @@ describe('duplicate-id', function () {
assert.lengthOf(checkContext._relatedNodes, 0);
});
- (shadowSupport.v1 ? it : xit)('should compare slotted content with the outer tree', function () {
+ (shadowSupport.v1 ? it : xit)('should compare slotted content with the light DOM', function () {
var node = document.createElement('div');
node.id = 'target';
node.innerHTML = '
text
';
From fcc51eb8b35be1a957767de9930b793dd15d1fa8 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Tue, 1 Aug 2017 17:01:20 +0200
Subject: [PATCH 103/142] fix: incomplete results should have impact
---
lib/core/utils/aggregateChecks.js | 4 ++--
lib/core/utils/aggregateRule.js | 14 +++++++++-----
test/core/utils/aggregateChecks.js | 18 ++++++++++++++++++
test/core/utils/aggregateRule.js | 11 +++++++++--
test/playground.html | 21 +++++++--------------
5 files changed, 45 insertions(+), 23 deletions(-)
diff --git a/lib/core/utils/aggregateChecks.js b/lib/core/utils/aggregateChecks.js
index dec080a003..fce52ecc15 100644
--- a/lib/core/utils/aggregateChecks.js
+++ b/lib/core/utils/aggregateChecks.js
@@ -1,3 +1,4 @@
+const { CANTTELL_PRIO, FAIL_PRIO } = axe.constants;
let checkMap = [];
checkMap[axe.constants.PASS_PRIO] = true;
checkMap[axe.constants.CANTTELL_PRIO] = null;
@@ -51,9 +52,8 @@ axe.utils.aggregateChecks = function (nodeResOriginal) {
nodeResult[type].forEach((check) => impacts.push(check.impact));
});
-
// for failed nodes, define the impact
- if (nodeResult.priority === axe.constants.FAIL_PRIO) {
+ if ([CANTTELL_PRIO, FAIL_PRIO].includes(nodeResult.priority)) {
nodeResult.impact = axe.utils.aggregate(axe.constants.impact, impacts);
} else {
nodeResult.impact = null;
diff --git a/lib/core/utils/aggregateRule.js b/lib/core/utils/aggregateRule.js
index 5cf07d3408..4d6f266ba7 100644
--- a/lib/core/utils/aggregateRule.js
+++ b/lib/core/utils/aggregateRule.js
@@ -35,11 +35,15 @@ axe.utils.aggregateRule = function (subResults) {
ruleResult[groupName].push(subResult);
});
- // Take the highest impact of failed rules
- var failGroup = axe.constants.FAIL_GROUP;
- if (ruleResult[failGroup].length > 0) {
- // Get the impact of all violations
- let impactList = ruleResult[failGroup]
+ // Take the highest impact of failed or canttell rules
+ var impactGroup = axe.constants.FAIL_GROUP;
+ if (ruleResult[impactGroup].length === 0) {
+ impactGroup = axe.constants.CANTTELL_GROUP;
+ }
+
+ if (ruleResult[impactGroup].length > 0) {
+ // Get the impact of all issues
+ let impactList = ruleResult[impactGroup]
.map((failure) => failure.impact);
ruleResult.impact = axe.utils.aggregate(axe.constants.impact, impactList) || null;
diff --git a/test/core/utils/aggregateChecks.js b/test/core/utils/aggregateChecks.js
index 0b67f2fb04..ad53f553ac 100644
--- a/test/core/utils/aggregateChecks.js
+++ b/test/core/utils/aggregateChecks.js
@@ -54,6 +54,24 @@ describe('axe.utils.aggregateChecks', function() {
});
});
+ it('returns impact for fail and canttell', function () {
+ var failCheck = axe.utils.aggregateChecks( createTestCheckResults({
+ any: [{ result: false, impact: 'serious' }]
+ }));
+ var canttellCheck = axe.utils.aggregateChecks( createTestCheckResults({
+ any: [{ result: undefined, impact: 'moderate' }]
+ }));
+
+ assert.equal(failCheck.impact, 'serious');
+ assert.equal(canttellCheck.impact, 'moderate');
+ });
+
+ it('sets impact to null for pass', function () {
+ var passCheck = axe.utils.aggregateChecks( createTestCheckResults({
+ any: [{ result: true, impact: 'serious' }]
+ }));
+ assert.isNull(passCheck.impact);
+ });
describe('none', function () {
it('gives result FAIL when any is true', function() {
diff --git a/test/core/utils/aggregateRule.js b/test/core/utils/aggregateRule.js
index 32ca67fa53..4f4f765f82 100644
--- a/test/core/utils/aggregateRule.js
+++ b/test/core/utils/aggregateRule.js
@@ -107,6 +107,13 @@ describe('axe.utils.aggregateRule', function() {
assert.lengthOf(ruleResult.passes, 1);
});
+ it('should provide impact on incomplete', function () {
+ var ruleResult = axe.utils.aggregateRule( createTestResults({
+ none: { result: undefined, impact: 'serious' }
+ }));
+ assert.equal(ruleResult.impact, 'serious');
+ });
+
it('should raise the highest "raisedMetadata" on failing checks', function() {
var ruleResult = axe.utils.aggregateRule( createTestResults({
none: { result: true, impact: 'moderate' },
@@ -116,12 +123,12 @@ describe('axe.utils.aggregateRule', function() {
{ result: false, impact: 'serious'}
]
},
- { none: { result: false, impact: 'critical' }},
+ { none: { result: undefined, impact: 'critical' }},
{ none: { result: false, impact: 'critical' }}
));
assert.equal(ruleResult.impact, 'serious');
assert.equal(ruleResult.violations[0].impact, 'serious');
+ assert.equal(ruleResult.incomplete[0].impact, 'critical');
assert.isNull(ruleResult.passes[0].impact);
- assert.isNull(ruleResult.passes[1].impact);
});
});
diff --git a/test/playground.html b/test/playground.html
index 94c489c015..98e016a17a 100644
--- a/test/playground.html
+++ b/test/playground.html
@@ -2,24 +2,17 @@
O hai
-
';
tree = axe.utils.getFlattenedTree(fixture);
assert.isTrue(
- hasContent(axe.utils.querySelectorAll(tree, '#target')[0])
+ hasContentVirtual(axe.utils.querySelectorAll(tree, '#target')[0])
);
});
- it('accepts DOM Nodes and virtual nodes', function () {
- fixture.innerHTML = '
text
';
- axe._tree = axe.utils.getFlattenedTree(fixture);
-
- // Virtual node
- assert.isTrue(
- hasContent(axe.utils.querySelectorAll(axe._tree, '#target')[0])
- );
- // DOM node
- assert.isTrue(
- hasContent(fixture.querySelector('#target'))
- );
- axe._tree = null;
- });
-
it('is false if the element does not show text', function () {
fixture.innerHTML = '';
tree = axe.utils.getFlattenedTree(fixture);
assert.isFalse(
- hasContent(axe.utils.querySelectorAll(tree, '#target')[0])
+ hasContentVirtual(axe.utils.querySelectorAll(tree, '#target')[0])
);
});
+ it('is called through hasContent, with a DOM node', function () {
+ var hasContent = axe.commons.dom.hasContent;
+ fixture.innerHTML = '
text
';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ assert.isTrue(hasContent(fixture.querySelector('#target')));
+
+ fixture.innerHTML = '';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ assert.isFalse(hasContent(fixture.querySelector('#target')));
+ });
+
it('is false if noRecursion is true and the content is not in a child', function () {
fixture.innerHTML = '
text
';
tree = axe.utils.getFlattenedTree(fixture);
- assert.isFalse(hasContent(axe.utils.querySelectorAll(tree, '#target')[0], true));
+ assert.isFalse(hasContentVirtual(axe.utils.querySelectorAll(tree, '#target')[0], true));
});
(shadowSupport ? it : xit)('looks at content of shadow dom elements', function () {
@@ -91,7 +87,7 @@ describe('dom.hasContent', function () {
tree = axe.utils.getFlattenedTree(fixture);
assert.isTrue(
- hasContent(axe.utils.querySelectorAll(tree, '#target')[0])
+ hasContentVirtual(axe.utils.querySelectorAll(tree, '#target')[0])
);
});
@@ -100,11 +96,9 @@ describe('dom.hasContent', function () {
var shadow = fixture.querySelector('#shadow').attachShadow({ mode: 'open' });
shadow.innerHTML = '
';
tree = axe.utils.getFlattenedTree(fixture);
- var node = axe.utils.querySelectorAll(tree, '.target');
- axe.log(tree, node);
assert.isTrue(
- hasContent(axe.utils.querySelectorAll(tree, '.target')[0])
+ hasContentVirtual(axe.utils.querySelectorAll(tree, '.target')[0])
);
});
});
diff --git a/test/commons/dom/is-offscreen.js b/test/commons/dom/is-offscreen.js
index b43bff763e..0479a03461 100644
--- a/test/commons/dom/is-offscreen.js
+++ b/test/commons/dom/is-offscreen.js
@@ -1,6 +1,7 @@
describe('dom.isOffscreen', function () {
'use strict';
var fixture = document.getElementById('fixture');
+ var shadowSupport = axe.testUtils.shadowSupport;
afterEach(function () {
fixture.innerHTML = '';
@@ -82,8 +83,8 @@ describe('dom.isOffscreen', function () {
var el = document.getElementById('target');
assert.isTrue(axe.commons.dom.isOffscreen(el));
-
});
+
it('should NOT detect elements positioned outside the right edge on LTR documents', function () {
fixture.innerHTML = '
Offscreen?
';
var el = document.getElementById('target');
@@ -91,8 +92,6 @@ describe('dom.isOffscreen', function () {
assert.isFalse(axe.commons.dom.isOffscreen(el));
});
-
-
it('should detect elements positioned outside the right edge on RTL documents', function () {
document.body.style.direction = 'rtl';
fixture.innerHTML = '
Offscreen?
';
@@ -100,6 +99,7 @@ describe('dom.isOffscreen', function () {
assert.isTrue(axe.commons.dom.isOffscreen(el));
});
+
it('should NOT detect elements positioned outside the left edge on RTL documents', function () {
document.body.style.direction = 'rtl';
fixture.innerHTML = '
Offscreen?
';
@@ -107,6 +107,7 @@ describe('dom.isOffscreen', function () {
assert.isFalse(axe.commons.dom.isOffscreen(el));
});
+
it('should not detect elements positioned because of a scroll', function () {
fixture.innerHTML = '
' +
'
goobye
' +
@@ -119,4 +120,23 @@ describe('dom.isOffscreen', function () {
scrollme.scrollIntoView();
assert.isFalse(axe.commons.dom.isOffscreen(viz));
});
+
+ (shadowSupport.v1 ? it : xit)('should detect on screen shadow nodes', function () {
+ fixture.innerHTML = '';
+ var shadow = fixture.querySelector('div').attachShadow({ mode: 'open' });
+ shadow.innerHTML = '
Offscreen?
';
+
+ var el = shadow.querySelector('#target');
+ assert.isFalse(axe.commons.dom.isOffscreen(el));
+ });
+
+ (shadowSupport.v1 ? it : xit)('should detect off screen shadow nodes', function () {
+ fixture.innerHTML = '';
+ var shadow = fixture.querySelector('div').attachShadow({ mode: 'open' });
+ shadow.innerHTML = '
Offscreen?
';
+
+ var el = shadow.querySelector('#target');
+ assert.isTrue(axe.commons.dom.isOffscreen(el));
+ });
+
});
diff --git a/test/commons/text/label.js b/test/commons/text/label-virtual.js
similarity index 78%
rename from test/commons/text/label.js
rename to test/commons/text/label-virtual.js
index 66fec2895d..c6283ea363 100644
--- a/test/commons/text/label.js
+++ b/test/commons/text/label-virtual.js
@@ -1,4 +1,4 @@
-describe('text.label', function () {
+describe('text.labelVirtual', function () {
'use strict';
var fixture = document.getElementById('fixture');
@@ -6,6 +6,15 @@ describe('text.label', function () {
fixture.innerHTML = '';
});
+ it('is called from text.label', function () {
+ fixture.innerHTML = '
monkeys
bananas
' +
+ '';
+
+ axe._tree = axe.utils.getFlattenedTree(document.body);
+ var target = fixture.querySelector('#target');
+ assert.equal(axe.commons.text.label(target), 'monkeys bananas');
+ });
+
describe('aria-labelledby', function () {
it('should join text with a single space', function () {
fixture.innerHTML = '
monkeys
bananas
' +
@@ -13,7 +22,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys bananas');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys bananas');
});
it('should filter invisible elements', function () {
@@ -22,7 +31,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys');
});
it('should take precedence over aria-label', function () {
@@ -31,7 +40,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys bananas');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys bananas');
});
it('should take precedence over explicit labels', function () {
@@ -41,7 +50,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys bananas');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys bananas');
});
it('should take precedence over implicit labels', function () {
@@ -51,7 +60,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys bananas');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys bananas');
});
it('should ignore whitespace only labels', function () {
@@ -60,7 +69,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.isNull(axe.commons.text.label(target));
+ assert.isNull(axe.commons.text.labelVirtual(target));
});
});
@@ -70,7 +79,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys');
});
it('should ignore whitespace only labels', function () {
@@ -78,7 +87,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.isNull(axe.commons.text.label(target));
+ assert.isNull(axe.commons.text.labelVirtual(target));
});
it('should take precedence over explicit labels', function () {
@@ -87,7 +96,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys');
});
it('should take precedence over implicit labels', function () {
@@ -96,7 +105,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys');
});
});
@@ -107,7 +116,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys');
});
it('should ignore whitespace only or empty labels', function () {
@@ -116,7 +125,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.isNull(axe.commons.text.label(target));
+ assert.isNull(axe.commons.text.labelVirtual(target));
});
it('should take precedence over implicit labels', function () {
@@ -126,7 +135,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys');
});
});
@@ -138,7 +147,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.equal(axe.commons.text.label(target), 'monkeys');
+ assert.equal(axe.commons.text.labelVirtual(target), 'monkeys');
});
it('should ignore whitespace only or empty labels', function () {
@@ -147,7 +156,7 @@ describe('text.label', function () {
var tree = axe._tree = axe.utils.getFlattenedTree(document.body);
var target = axe.utils.querySelectorAll(tree, '#target')[0];
- assert.isNull(axe.commons.text.label(target));
+ assert.isNull(axe.commons.text.labelVirtual(target));
});
});
});
diff --git a/test/commons/text/visible.js b/test/commons/text/visible-virtual.js
similarity index 79%
rename from test/commons/text/visible.js
rename to test/commons/text/visible-virtual.js
index 11b46d79bb..a948d69507 100644
--- a/test/commons/text/visible.js
+++ b/test/commons/text/visible-virtual.js
@@ -3,6 +3,7 @@ describe('text.visible', function () {
var fixture = document.getElementById('fixture');
var shadowSupported = axe.testUtils.shadowSupport.v1;
+ var visibleVirtual = axe.commons.text.visibleVirtual;
afterEach(function () {
document.getElementById('fixture').innerHTML = '';
@@ -12,25 +13,25 @@ describe('text.visible', function () {
it('should not return elements with visibility: hidden', function () {
fixture.innerHTML = 'HelloHi';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0]), 'Hello');
+ assert.equal(visibleVirtual(tree[0]), 'Hello');
});
it('should handle implicitly recursive calls', function () {
fixture.innerHTML = 'HelloHi';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0]), 'HelloHi');
+ assert.equal(visibleVirtual(tree[0]), 'HelloHi');
});
it('should handle explicitly recursive calls', function () {
fixture.innerHTML = 'HelloHi';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0], null, false), 'HelloHi');
+ assert.equal(visibleVirtual(tree[0], null, false), 'HelloHi');
});
it('should handle non-recursive calls', function () {
fixture.innerHTML = 'HelloHi';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0], null, true), 'Hello');
+ assert.equal(visibleVirtual(tree[0], null, true), 'Hello');
});
it('should know how visibility works', function () {
@@ -39,20 +40,20 @@ describe('text.visible', function () {
'';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0]), 'Hello Hi');
+ assert.equal(visibleVirtual(tree[0]), 'Hello Hi');
});
it('should not return elements with display: none', function () {
fixture.innerHTML = 'HelloHi';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0]), 'Hello');
+ assert.equal(visibleVirtual(tree[0]), 'Hello');
});
it('should trim the result', function () {
fixture.innerHTML = ' \u00A0 Hello \r\n Hi \n \n \n ';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0]), 'Hello Hi');
+ assert.equal(visibleVirtual(tree[0]), 'Hello Hi');
});
it('should ignore script and style tags', function () {
@@ -60,17 +61,16 @@ describe('text.visible', function () {
'Hello';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0]), 'Hello');
+ assert.equal(visibleVirtual(tree[0]), 'Hello');
});
-
it('should not take into account position of parents', function () {
fixture.innerHTML = '
' +
'
Hello
' +
'
';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0]), 'Hello');
+ assert.equal(visibleVirtual(tree[0]), 'Hello');
});
it('should correctly handle slotted elements', function () {
@@ -89,7 +89,7 @@ describe('text.visible', function () {
fixture.innerHTML = '
';
makeShadowTree(fixture.firstChild);
var tree = axe.utils.getFlattenedTree(fixture.firstChild);
- assert.equal(axe.commons.text.visible(tree[0]), 'Stuffhello');
+ assert.equal(visibleVirtual(tree[0]), 'Stuffhello');
}
});
});
@@ -99,7 +99,7 @@ describe('text.visible', function () {
fixture.innerHTML = 'HelloHi';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0], true), 'Hello');
+ assert.equal(visibleVirtual(tree[0], true), 'Hello');
});
it('should know how visibility works', function () {
@@ -108,21 +108,20 @@ describe('text.visible', function () {
'';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0], true), 'Hello Hi');
+ assert.equal(visibleVirtual(tree[0], true), 'Hello Hi');
});
-
it('should not return elements with display: none', function () {
fixture.innerHTML = 'HelloHi';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0], true), 'Hello');
+ assert.equal(visibleVirtual(tree[0], true), 'Hello');
});
it('should trim the result', function () {
fixture.innerHTML = ' \u00A0 Hello \r\n Hi \n \n \n ';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0], true), 'Hello Hi');
+ assert.equal(visibleVirtual(tree[0], true), 'Hello Hi');
});
it('should ignore script and style tags', function () {
@@ -130,30 +129,26 @@ describe('text.visible', function () {
'Hello';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0], true), 'Hello');
+ assert.equal(visibleVirtual(tree[0], true), 'Hello');
});
-
it('should not consider offscreen text as hidden (position)', function () {
fixture.innerHTML = '
' +
'
Hello
' +
'
';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0], true), 'Hello');
+ assert.equal(visibleVirtual(tree[0], true), 'Hello');
});
-
it('should not consider offscreen text as hidden (text-indent)', function () {
fixture.innerHTML = '
' +
'Hello
';
var tree = axe.utils.getFlattenedTree(fixture);
- assert.equal(axe.commons.text.visible(tree[0], true), 'Hello');
-
+ assert.equal(visibleVirtual(tree[0], true), 'Hello');
});
});
-
});
From ebb2a5d545fae5f309dbc1efa2780e89d29e6597 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Mon, 7 Aug 2017 17:47:13 +0200
Subject: [PATCH 107/142] feat(aria): Support progressive ARIA 1.1 attributes /
roles (#468)
Add support for role=feed, role=term, aria-placeholder, aria-modal,
aria-current, aria-keyshortcuts
---
lib/commons/aria/index.js | 58 +++++++++++++++++++++++++++++++--------
1 file changed, 46 insertions(+), 12 deletions(-)
diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js
index 5805de6747..3a67810fe3 100644
--- a/lib/commons/aria/index.js
+++ b/lib/commons/aria/index.js
@@ -71,6 +71,9 @@ lookupTables.attributes = {
type: 'nmtoken',
values: ['true', 'false', 'spelling', 'grammar']
},
+ 'aria-keyshortcuts': {
+ type: 'string'
+ },
'aria-label': {
type: 'string'
},
@@ -84,6 +87,10 @@ lookupTables.attributes = {
type: 'nmtoken',
values: ['off', 'polite', 'assertive']
},
+ 'aria-modal': {
+ type: 'boolean',
+ values: ['true', 'false']
+ },
'aria-multiline': {
type: 'boolean',
values: ['true', 'false']
@@ -99,6 +106,9 @@ lookupTables.attributes = {
'aria-owns': {
type: 'idrefs'
},
+ 'aria-placeholder': {
+ type: 'string'
+ },
'aria-posinset': {
type: 'int'
},
@@ -155,7 +165,7 @@ lookupTables.attributes = {
lookupTables.globalAttributes = [
'aria-atomic', 'aria-busy', 'aria-controls', 'aria-current', 'aria-describedby',
'aria-disabled', 'aria-dropeffect', 'aria-flowto', 'aria-grabbed',
- 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-label',
+ 'aria-haspopup', 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label',
'aria-labelledby', 'aria-live', 'aria-owns', 'aria-relevant'
];
@@ -172,7 +182,7 @@ lookupTables.role = {
'alertdialog': {
type: 'widget',
attributes: {
- allowed: ['aria-expanded']
+ allowed: ['aria-expanded', 'aria-modal']
},
owned: null,
nameFrom: ['author'],
@@ -240,7 +250,8 @@ lookupTables.role = {
'columnheader': {
type: 'structure',
attributes: {
- allowed: ['aria-expanded', 'aria-sort', 'aria-readonly', 'aria-selected', 'aria-required']
+ allowed: ['aria-colindex', 'aria-colspan', 'aria-expanded', 'aria-rowindex', 'aria-rowspan',
+ 'aria-required', 'aria-readonly', 'aria-selected', 'aria-sort']
},
owned: null,
nameFrom: ['author', 'contents'],
@@ -294,12 +305,12 @@ lookupTables.role = {
owned: null,
nameFrom: ['author'],
context: null,
- implicit: ['dd']
+ implicit: ['dd', 'dfn']
},
'dialog': {
type: 'widget',
attributes: {
- allowed: ['aria-expanded']
+ allowed: ['aria-expanded', 'aria-modal']
},
owned: null,
nameFrom: ['author'],
@@ -325,6 +336,17 @@ lookupTables.role = {
context: null,
implicit: ['body']
},
+ 'feed': {
+ type: 'structure',
+ attributes: {
+ allowed: ['aria-expanded']
+ },
+ owned: {
+ one: ['article']
+ },
+ nameFrom: ['author'],
+ context: null
+ },
'form': {
type: 'landmark',
attributes: {
@@ -350,7 +372,8 @@ lookupTables.role = {
'gridcell': {
type: 'widget',
attributes: {
- allowed: ['aria-selected', 'aria-readonly', 'aria-expanded', 'aria-required']
+ allowed: ['aria-colindex', 'aria-colspan', 'aria-expanded', 'aria-rowindex',
+ 'aria-rowspan', 'aria-selected', 'aria-readonly', 'aria-required']
},
owned: null,
nameFrom: ['author', 'contents'],
@@ -620,7 +643,7 @@ lookupTables.role = {
'row': {
type: 'structure',
attributes: {
- allowed: ['aria-level', 'aria-selected', 'aria-activedescendant', 'aria-expanded']
+ allowed: ['aria-activedescendant', 'aria-colcount', 'aria-expanded', 'aria-level', 'aria-selected', 'aria-rowcount']
},
owned: {
one: ['cell', 'columnheader', 'rowheader', 'gridcell']
@@ -644,7 +667,8 @@ lookupTables.role = {
'rowheader': {
type: 'structure',
attributes: {
- allowed: ['aria-sort', 'aria-required', 'aria-readonly', 'aria-expanded', 'aria-selected']
+ allowed: ['aria-colindex', 'aria-colspan', 'aria-expanded', 'aria-rowindex', 'aria-rowspan',
+ 'aria-required', 'aria-readonly', 'aria-selected', 'aria-sort']
},
owned: null,
nameFrom: ['author', 'contents'],
@@ -673,7 +697,7 @@ lookupTables.role = {
'searchbox': {
type: 'widget',
attributes: {
- allowed: ['aria-activedescendant', 'aria-autocomplete', 'aria-multiline', 'aria-readonly', 'aria-required']
+ allowed: ['aria-activedescendant', 'aria-autocomplete', 'aria-multiline', 'aria-readonly', 'aria-required', 'aria-placeholder']
},
owned: null,
nameFrom: ['author'],
@@ -787,6 +811,16 @@ lookupTables.role = {
nameFrom: ['author'],
context: null
},
+ 'term': {
+ type: 'structure',
+ attributes: {
+ allowed: ['aria-expanded']
+ },
+ owned: null,
+ nameFrom: ['author', 'contents'],
+ context: null,
+ implicit: ['dt']
+ },
'text': {
type: 'structure',
owned: null,
@@ -796,7 +830,7 @@ lookupTables.role = {
'textbox': {
type: 'widget',
attributes: {
- allowed: ['aria-activedescendant', 'aria-autocomplete', 'aria-multiline', 'aria-readonly', 'aria-required']
+ allowed: ['aria-activedescendant', 'aria-autocomplete', 'aria-multiline', 'aria-readonly', 'aria-required', 'aria-placeholder']
},
owned: null,
nameFrom: ['author'],
@@ -847,8 +881,8 @@ lookupTables.role = {
'treegrid': {
type: 'composite',
attributes: {
- allowed: ['aria-activedescendant', 'aria-expanded', 'aria-level', 'aria-multiselectable',
- 'aria-readonly', 'aria-required']
+ allowed: ['aria-activedescendant', 'aria-colcount', 'aria-expanded', 'aria-level', 'aria-multiselectable',
+ 'aria-readonly', 'aria-required', 'aria-rowcount']
},
owned: {
all: ['treeitem']
From 57ccbbd6fdd9245a34132cbd201e130e30a66893 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Tue, 8 Aug 2017 14:34:09 -0700
Subject: [PATCH 108/142] Shadow DOM docs (#471)
* docs(API): change mentions of a11yCheck to axe.run
* docs: add Shadow DOM documentation
Closes https://github.com/dequelabs/axe-core/issues/401
* feat: add missing accessibleTextVirtual method
Closes https://github.com/dequelabs/axe-core/issues/472
* docs: document testUtils and a few commons
* docs: move core/test APIs to developer guide
---
CONTRIBUTING.md | 7 +
doc/API.md | 96 ++++++-
doc/developer-guide.md | 267 +++++++++++++++++-
lib/checks/label/duplicate-img-label.js | 2 +-
...ble-text.js => accessible-text-virtual.js} | 7 +-
lib/core/utils/flattened-tree.js | 12 +-
test/commons/text/accessible-text.js | 210 +++++++-------
test/testutils.js | 20 ++
8 files changed, 505 insertions(+), 116 deletions(-)
rename lib/commons/text/{accessible-text.js => accessible-text-virtual.js} (95%)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0fd3b4b33e..1e5a0b49d0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -28,6 +28,13 @@ That having been said, we prefer:
5. Use of 'use strict'
6. Variables declared at the top of functions
+### Shadow DOM
+
+For any proposed changes to rules, checks, commons, or other APIs to be accepted
+in axe-core,your code must support Shadow DOM. See [API.md](./doc/API.md) and the
+[developer guide](./doc/developer-guide.md) for documentation on the available methods
+and test utilities.
+
### Testing
We expect all code to be 100% covered by tests. We don't have or want code coverage metrics but we will review tests and suggest changes when we think the test(s) do(es) not adequately exercise the code/code changes.
diff --git a/doc/API.md b/doc/API.md
index d641edc1e9..bb7efc8eda 100644
--- a/doc/API.md
+++ b/doc/API.md
@@ -21,7 +21,10 @@
1. [API Name: axe.registerPlugin](#api-name-axeregisterplugin)
1. [API Name: axe.cleanup](#api-name-axecleanup)
1. [API Name: axe.a11yCheck](#api-name-axea11ycheck)
-1. [Section 3: Example Reference](#section-3-example-reference)
+ 1. [Virtual DOM Utilities](#virtual-dom-utilities)
+ 1. [API Name: axe.utils.querySelectorAll](#api-name-axeutilsqueryselectorall)
+ 1. [Common Functions](#common-functions)
+1. [Section 3: Example Reference](#section-5-example-reference)
## Section 1: Introduction
@@ -450,7 +453,7 @@ 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 four components – a `passes` array, a `violations` array, an `incomplete` array and an `inapplicable` array.
+The callback function passed in as the third parameter of `axe.run` 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.
@@ -483,7 +486,7 @@ Each object returned in these arrays have the following properties:
* `helpUrl` - URL that provides more information about the specifics of the violation. Links to a page on the Deque University site.
* `id` - Unique identifier for the rule; [see the list of rules](rule-descriptions.md)
* `impact` - How serious the violation is. Can be one of "minor", "moderate", "serious", or "critical" if the Rule failed or `null` if the check passed
-* `tags` - Array of tags that this rule is assigned. These tags can be used in the option structure to select which rules are run ([see `axe.a11yCheck` parameters below for more information](#a11ycheck-parameters)).
+* `tags` - Array of tags that this rule is assigned. These tags can be used in the option structure to select which rules are run ([see `axe.run` parameters for more information](#parameters-axerun)).
* `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
@@ -608,6 +611,93 @@ In axe-core v1 the main method for axe was `axe.a11yCheck()`. This method was re
- .a11yCheck requires a context object, and so will not fall back to the document root.
- .a11yCheck does not return a Promise.
+### Virtual DOM Utilities
+
+Note: If you’re writing rules or checks, you’ll have both the `node` and `virtualNode` passed in.
+But if you need to query the flattened tree, the documented function below should help. See the
+[developer guide](./developer-guide.md) for more information.
+
+#### API Name: axe.utils.querySelectorAll
+
+##### Description
+
+A querySelectorAll implementation that works on the virtual DOM and Shadow DOM by manually walking the flattened tree instead of relying on DOM API methods which don’t step into Shadow DOM.
+
+Note: while there is no `axe.utils.querySelector` method, you can reproduce that behavior by accessing the first item returned in the array.
+
+##### Synopsis
+
+```javascript
+axe.utils.querySelectorAll(virtualNode, 'a[href]');
+```
+
+##### Parameters
+
+* `virtualNode` – object, the flattened DOM tree to query against. `axe._tree` is available for this purpose during an audit; see below.
+* `selector` – string, the CSS selector to use as a filter. For the most part, this should work seamlessly with `document.querySelectorAll`.
+
+##### Returns
+
+An Array of filtered HTML nodes.
+
+
+### Common Functions
+
+#### axe.commons.dom.getComposedParent
+
+Get an element's parent in the flattened tree
+
+##### Synopsis
+
+```javascript
+axe.commons.dom.getComposedParent(node)
+```
+
+##### Parameters
+* `element` – HTMLElement. The element for which you want to find a parent
+
+##### Returns
+
+A DOMNode for the parent
+
+
+#### axe.commons.dom.getRootNode
+
+Return the document or document fragment (shadow DOM)
+
+##### Synopsis
+
+```javascript
+axe.commons.dom.getRootNode(node)
+```
+
+##### Parameters
+* `element` – HTMLElement. The element for which you want to find the root node
+
+##### Returns
+
+The top-level document or shadow DOM document fragment
+
+
+#### axe.commons.dom.findUp
+
+Recusively walk up the DOM, checking for a node which matches a selector. Warning: this should be used sparingly for performance reasons.
+
+##### Synopsis
+
+```javascript
+axe.commons.dom.findUp(node, '.selector')
+```
+
+##### Parameters
+* `element` – HTMLElement. The starting element
+* `selector` – String. The target selector for the HTMLElement
+
+##### Returns
+
+Either the matching HTMLElement or `null` if there was no match.
+
+
## Section 3: Example Reference
This package contains examples for [jasmine](examples/jasmine), [mocha](examples/mocha), [phantomjs](examples/phantomjs), [qunit](examples/qunit), [selenium using javascript](examples/selenium), and [generating HTML from the violations array](examples/html-handlebars.md). Each of these examples is in the [doc/examples](examples) folder. In each folder, there is a README.md file which contains specific information about each example.
diff --git a/doc/developer-guide.md b/doc/developer-guide.md
index 6d8da87670..7e0125af99 100644
--- a/doc/developer-guide.md
+++ b/doc/developer-guide.md
@@ -2,6 +2,22 @@
aXe runs a series of tests to check for accessibility of content and functionality on a website. A test is made up of a series of Rules which are, themselves, made up of Checks. aXe executes these Rules asynchronously and, when the Rules are finished running, runs a callback function which is passed a Result structure. Since some Rules run on the page level while others do not, tests will also run in one of two ways. If a document is specified, the page level rules will run, otherwise they will not.
+1. [Getting Started](#getting-started)
+1. [Architecture Overview](#architecture-overview)
+ 1. [Rules](#rules)
+ 1. [Checks](#checks)
+ 1. [Common Functions](#common-functions)
+ 1. [Virtual Nodes](#virtual-nodes)
+ 1. [Core Utilities](#core-utilities)
+1. [Virtual DOM APIs](#virtual-dom-apis)
+ 1. [API Name: axe.utils.getFlattenedTree](#api-name-axeutilsgetflattenedtree)
+ 1. [API Name: axe.utils.getNodeFromTree](#api-name-axeutilsgetnodefromtree)
+1. [Test Utilities](#test-utilities)
+ 1. [Test Util Name: axe.testUtils.MockCheckContext](#test-util-name-axetestutilsmockcheckcontext)
+ 1. [Test Util Name: axe.testUtils.shadowSupport](#test-util-name-axetestutilsshadowsupport)
+ 1. [Test Util Name: axe.testUtils.fixtureSetup](#test-util-name-axetestutilsfixturesetup)
+ 1. [Test Util Name: axe.testUtils.checkSetup](#test-util-name-axetestutilschecksetup)
+
## Getting Started
### Environment Pre-requisites
@@ -16,9 +32,9 @@ To build axe.js, simply run `grunt build`. axe.js and axe.min.js are placed int
### Running Tests
-To run all tests from the command line you can run `grunt test`, which will run all unit and integration tests using PhantomJS.
+To run all tests from the command line you can run `grunt test`, which will run all unit and integration tests using PhantomJS and Selenium Webdriver.
-You can also load tests in any supported browser, which is helpful for debugging. Tests require a local server to run, you must first start a local server to serve files. You can use Grunt to start one by running `grunt connect watch`. Once your local server is running you can load the following pages in any browser to run tests:
+You can also load tests in any supported browser, which is helpful for debugging. Tests require a local server to run, you must first start a local server to serve files. You can use Grunt to start one by running `grunt dev`. Once your local server is running you can load the following pages in any browser to run tests:
1. [Core Tests](../test/core/)
@@ -41,7 +57,7 @@ After execution, a Check will return `true` or `false` depending on whether or n
Rules are defined by JSON files in the [lib/rules directory](../lib/rules). The JSON object is used to seed the [Rule object](../lib/core/base/rule.js#L30). A valid Rule JSON consists of the following:
* `id` - `String` A unique name of the Rule.
-* `selector` - **optional** `String` which is a CSS selector that specifies the elements of the page on which the Rule runs. If omitted, the rule will run against every node.
+* `selector` - **optional** `String` which is a CSS selector that specifies the elements of the page on which the Rule runs. aXe-core will look inside of the light DOM and *open* [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) trees for elements matching the provided selector. If omitted, the rule will run against every node.
* `excludeHidden` - **optional** `Boolean` Whether the rule should exclude hidden elements. Defaults to `true`.
* `enabled` - **optional** `Boolean` Whether the rule is enabled by default. Defaults to `true`.
* `pageLevel` - **optional** `Boolean` Whether the rule is page level. Page level rules will only run if given an entire `document` as context.
@@ -63,7 +79,12 @@ There is a Grunt target which will ensure each Rule has a valid format, which ca
#### Matches Function
-Custom `matches` functions are executed against each node which matches the Rule's `selector` and receive a single parameter named `node`, which is the Node to test. The function must return either `true` or `false`. Common functions are provided as `commons`. [See the data-table matches function for an example.](../lib/rules/data-table-matches.js)
+Custom `matches` functions are executed against each node which matches the Rule's `selector` and receive two parameters:
+
+* `node` – node, the DOM Node to test
+* `virtualNode`– object, the virtual DOM representation of the node. See [virtualNode documentation](#virtual-nodes) for more.
+
+The matches function must return either `true` or `false`. Common functions are provided as `commons`. [See the data-table matches function for an example.](../lib/rules/data-table-matches.js)
### Checks
@@ -87,6 +108,7 @@ The following variables are defined for `Check#evaluate`:
* `node` - `HTMLElement` The element that the Check is run against
* `options` - `Mixed` Any options specific to this Check that may be necessary. If not specified by the user at run-time or configure-time; it will use `options` as defined by the Check's JSON file.
+* `virtualNode` – `Object` The virtualNode object for use with Shadow DOM. See [virtualNode documentation](#virtual-nodes).
* `this.data()` - `Function` Free-form data that either the Check message requires or is presented as `data` in the CheckResult object. Subsequent calls to `this.data()` will overwrite previous. See [aria-valid-attr](../lib/checks/aria/valid-attr.js) for example usage.
* `this.relatedNodes()` - `Function` Array or NodeList of elements that are related to this Check. For example the [duplicate-id](../lib/checks/shared/duplicate-id.js) Check will add all Elements which share the same ID.
* `commons` - Common functions that may be used across multiple Checks. See [Common Functions](#common-functions) for more information.
@@ -115,9 +137,9 @@ return results.filter(function (r) {
});
```
-#### Pass and Fail Templates
+#### Pass, Fail and Incomplete Templates
-Occasionally, you may want to add additional information about why a Check passed or failed into its message. For example, the [aria-valid-attr](../lib/checks/aria/valid-attr.json) will add information about any invalid ARIA attributes to its fail message. The message uses the [doT.js](http://olado.github.io/doT/) and is compiled to a JavaScript function at build-time. In the Check message, you have access to the `CheckResult` as `it`.
+Occasionally, you may want to add additional information about why a Check passed, failed or returned undefined into its message. For example, the [aria-valid-attr](../lib/checks/aria/valid-attr.json) will add information about any invalid ARIA attributes to its fail message. The message uses the [doT.js](http://olado.github.io/doT/) and is compiled to a JavaScript function at build-time. In the Check message, you have access to the `CheckResult` as `it`.
#### CheckResult
@@ -131,7 +153,54 @@ A CheckResult has the following properties:
### Common Functions
-Common functions are an internal library used by the rules and checks. If you have code repeated across rules and checks, you can use these functions and contribute to them. They are made available to every function as `commons`. Documentation is available in [source code](../lib/commons/).
+Common functions are an internal library used by the rules and checks. If you have code repeated across rules and checks, you can use these functions and contribute to them. They are made available to every function as `commons`. Documentation is available in [source code](../lib/commons/).
+
+#### Commons and Shadow DOM
+
+To support Shadow DOM while maintaining backwards compatibility, commons functions that
+query DOM nodes must operate on an in-memory representation of the DOM using aXe-core’s
+built-in [API methods and utility functions](./API.md#virtual-dom-utilities).
+
+Commons functions should do the virtual tree lookup and call a `virtual` function
+including the rest of the commons code. The naming of this special function
+should contain the original commons function name with `Virtual` added to signify
+it expects to operate on a virtual DOM tree.
+
+Let’s look at an example:
+
+```javascript
+axe.commons.text.accessibleText = function (element, inLabelledbyContext) {
+ let virtualNode = axe.utils.getNodeFromTree(axe._tree[0], element); // throws an exception on purpose if axe._tree not correct
+ return axe.commons.text.accessibleTextVirtual(virtualNode, inLabelledbyContext);
+}
+
+axe.commons.text.accessibleTextVirtual = function (element, inLabelledbyContext) {
+ // rest of the commons code minus the virtual tree lookup, since it’s passed in
+}
+```
+
+`accessibleTextVirtual` would only be called directly if you’ve got a virtual node
+you can use. If you don’t already have one, call the `accessibleText` lookup function,
+which passes on a virtual DOM node with both the light DOM and Shadow DOM (if applicable).
+
+### Virtual Nodes
+
+To support Shadow DOM, aXe-core has the ability to handle virtual nodes in [rule matches](#matches-function)
+and [check evaluate](#check-evaluate) functions. The full set of API methods for Shadow DOM can be
+found in the [API documentation](./API.md#virtual-dom-utilities), but the general
+structure for a virtualNode is as follows:
+
+```javascript
+{
+ actualNode: ,
+ children: ,
+ shadowId:
+}
+```
+
+- A virtualNode is an object containing an HTML DOM element (`actualNode`).
+- Children contains an array of child virtualNodes.
+- The shadowID indicates whether the node is in a shadow root and if it is, which one it is inside the boundary.
### Core Utilities
@@ -163,3 +232,187 @@ Elements returned by the DqElement class have the following methods and properti
* `source` - `string` The generated HTML source code of the element
* `element` - `DOMNode` The element which this object is based off or the containing frame, used for sorting.
* `toJSON()` - Returns an object containing the selector and source properties
+
+
+## Virtual DOM APIs
+
+Note: You shouldn’t need the Shadow DOM APIs below unless you’re working on the axe-core
+engine, as rules and checks already have `virtualNode` objects passed in. However, these APIs
+will make it easier to work with the virtual DOM.
+
+### API Name: axe.utils.getFlattenedTree
+
+#### Description
+
+Recursvely return an array containing the virtual DOM tree for the node specified, excluding comment nodes
+and shadow DOM nodes `` and ``. This method will return a flattened tree containing both
+light and shadow DOM, if applicable.
+
+#### Synopsis
+
+```javascript
+var element = document.body;
+axe.utils.getFlattenedTree(element, shadowId)
+```
+
+#### Parameters
+ - `node` – HTMLElement. The current HTML node for which you want a flattened DOM tree.
+ - `shadowId` – string(optional). ID of the shadow DOM that is the closest shadow ancestor of the node
+
+#### Returns
+
+An array of objects, where each object is a virtualNode:
+
+```javascript
+[{
+ actualNode: body,
+ children: [virtualNodes],
+ shadowId: undefined
+}]
+```
+
+### API Name: axe.utils.getNodeFromTree
+
+#### Description
+
+Recursively return a single node from a virtual DOM tree. This is commonly used in rules and checks where the node is readily available without querying the DOM.
+
+#### Synopsis
+
+```javascript
+axe.utils.getNodeFromTree(axe._tree[0], node);
+```
+
+#### Parameters
+
+ - `vNode` – object. The flattened DOM tree to fetch a virtual node from
+ - `node` – HTMLElement. The HTML DOM node for which you need a virtual representation
+
+#### Returns
+
+A virtualNode object:
+
+```javascript
+{
+ actualNode: div,
+ children: [virtualNodes],
+ shadowId: undefined
+}
+```
+
+## Test Utilities
+
+All tests must support Shadow DOM, so we created some test utilities to make this easier.
+
+### Test Util Name: MockCheckContext
+
+Create a check context for mocking and resetting data and relatedNodes in tests.
+
+#### Synopsis
+
+```javascript
+describe('region', function () {
+ var fixture = document.getElementById('fixture');
+
+ var checkContext = new axe.testUtils.MockCheckContext();
+
+ afterEach(function () {
+ fixture.innerHTML = '';
+ checkContext.reset();
+ });
+
+ it('should return true when all content is inside the region', function () {
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.equal(checkContext._relatedNodes.length, 0);
+ });
+});
+```
+
+#### Parameters
+
+None
+
+#### Returns
+
+An object containg the data, relatedNodes, and a way to reset them.
+
+```javascript
+{
+ data: (){},
+ relatedNodes: (){},
+ reset: (){}
+}
+```
+
+### Test Util Name: shadowSupport
+
+Provide an API for determining Shadow DOM v0 and v1 support in tests. PhantomJS doesn't have Shadow DOM support, while some browsers do.
+
+#### Synopsis
+
+```javascript
+(axe.testUtils.shadowSupport.v1 ? it : xit)('should test Shadow tree content', function () {
+ // The rest of the shadow DOM test
+});
+```
+
+#### Parameters
+
+None
+
+#### Returns
+
+An object containing booleans for the following Shadow DOM supports: `v0`, `v1`, or `undefined`.
+
+### Test Util Name: fixtureSetup
+
+Method for injecting content into a fixture and caching the flattened DOM tree (light and Shadow DOM together).
+
+#### Synopsis
+
+```javascript
+it('should return true if there is only one ' + type + ' element with the same name', function () {
+ axe.testUtils.fixtureSetup('' +
+ '');
+
+ var node = fixture.querySelector('#target');
+ assert.isTrue(check.evaluate.call(checkContext, node));
+});
+```
+
+#### Parameters
+
+* `content` – Node|String. Stuff to go into the fixture (html or DOM node)
+
+#### Returns
+
+An HTML Element for the fixture
+
+### Test Util Name: checkSetup
+
+Create check arguments.
+
+#### Synopsis
+
+```javascript
+it('should return true when all content is inside the region', function () {
+ var checkArgs = checkSetup('
');
+
+ assert.isTrue(checks.region.evaluate.apply(checkContext, checkArgs));
+ assert.equal(checkContext._relatedNodes.length, 0);
+});
+```
+
+#### Parameters
+
+* `content` – String|Node. Stuff to go into the fixture (html or node)
+* `options` – Object. Options argument for the check (optional, default: {})
+* `target` – String. Target for the check, CSS selector (default: '#target')
+
+#### Returns
+
+An array with the DOM Node, options and virtualNode
+
+```javascript
+[node, options, virtualNode]
+```
diff --git a/lib/checks/label/duplicate-img-label.js b/lib/checks/label/duplicate-img-label.js
index 432f197ae8..e9bd2bfd96 100644
--- a/lib/checks/label/duplicate-img-label.js
+++ b/lib/checks/label/duplicate-img-label.js
@@ -12,5 +12,5 @@ const images = axe.utils.querySelectorAll(virtualNode, 'img')
// See if any of the images duplicate the node's text
return images.some(img =>
- text === axe.commons.text.accessibleText(img).toLowerCase()
+ text === axe.commons.text.accessibleTextVirtual(img).toLowerCase()
);
diff --git a/lib/commons/text/accessible-text.js b/lib/commons/text/accessible-text-virtual.js
similarity index 95%
rename from lib/commons/text/accessible-text.js
rename to lib/commons/text/accessible-text-virtual.js
index ed07a7df41..db9ce355d4 100644
--- a/lib/commons/text/accessible-text.js
+++ b/lib/commons/text/accessible-text-virtual.js
@@ -141,7 +141,12 @@ function nonEmptyText(t) {
* @param {Boolean} inLabelledByContext True when in the context of resolving a labelledBy
* @return {string}
*/
-text.accessibleText = function(element, inLabelledByContext) {
+text.accessibleText = function accessibleText(element, inLabelledByContext) {
+ let virtualNode = axe.utils.getNodeFromTree(axe._tree[0], element); // throws an exception on purpose if axe._tree not correct
+ return axe.commons.text.accessibleTextVirtual(virtualNode, inLabelledByContext);
+};
+
+text.accessibleTextVirtual = function accessibleTextVirtual(element, inLabelledByContext) {
let accessibleNameComputation;
const encounteredNodes = [];
if (element instanceof Node) {
diff --git a/lib/core/utils/flattened-tree.js b/lib/core/utils/flattened-tree.js
index 2fc6b192a6..0c5189f325 100644
--- a/lib/core/utils/flattened-tree.js
+++ b/lib/core/utils/flattened-tree.js
@@ -52,9 +52,8 @@ function getSlotChildren(node) {
}
/**
- * recursvely returns an array of the virtual DOM nodes at this level
- * excluding comment nodes and of course the shadow DOM nodes
- * and
+ * Recursvely returns an array of the virtual DOM nodes at this level
+ * excluding comment nodes and 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
@@ -120,6 +119,13 @@ axe.utils.getFlattenedTree = function (node, shadowId) {
}
};
+
+/**
+ * Recursively return a single node from a virtual dom tree
+ *
+ * @param {Object} vNode The flattened, virtual DOM tree
+ * @param {Node} node The HTML DOM node
+ */
axe.utils.getNodeFromTree = function (vNode, node) {
var found;
diff --git a/test/commons/text/accessible-text.js b/test/commons/text/accessible-text.js
index 2cbd9eef73..84c0ea89f9 100644
--- a/test/commons/text/accessible-text.js
+++ b/test/commons/text/accessible-text.js
@@ -1,4 +1,4 @@
-describe('text.accessibleText', function() {
+describe('text.accessibleTextVirtual', function() {
'use strict';
var fixture = document.getElementById('fixture');
@@ -9,6 +9,14 @@ describe('text.accessibleText', function() {
axe._tree = null;
});
+ it('is called through accessibleText with a DOM node', function() {
+ var accessibleText = axe.commons.text.accessibleText;
+ fixture.innerHTML = '';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+ var target = fixture.querySelector('input');
+ assert.equal(accessibleText(target), '');
+ });
+
it('should match the first example from the ARIA spec', function() {
fixture.innerHTML = '
' +
' ' +
@@ -27,8 +35,8 @@ describe('text.accessibleText', function() {
var rule2a = axe.utils.querySelectorAll(axe._tree, '#rule2a')[0];
var rule2c = axe.utils.querySelectorAll(axe._tree, '#rule2c')[0];
- assert.equal(axe.commons.text.accessibleText(rule2a), 'File');
- assert.equal(axe.commons.text.accessibleText(rule2c), 'New');
+ assert.equal(axe.commons.text.accessibleTextVirtual(rule2a), 'File');
+ assert.equal(axe.commons.text.accessibleTextVirtual(rule2c), 'New');
});
it('should match the second example from the ARIA spec', function() {
@@ -50,8 +58,8 @@ describe('text.accessibleText', function() {
var rule2a = axe.utils.querySelectorAll(axe._tree, '#beep')[0];
var rule2b = axe.utils.querySelectorAll(axe._tree, '#flash')[0];
- assert.equal(axe.commons.text.accessibleText(rule2a), 'Beep');
- assert.equal(axe.commons.text.accessibleText(rule2b), 'Flash the screen 3 times');
+ assert.equal(axe.commons.text.accessibleTextVirtual(rule2a), 'Beep');
+ assert.equal(axe.commons.text.accessibleTextVirtual(rule2b), 'Flash the screen 3 times');
});
@@ -64,7 +72,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is a label');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is a label');
});
it('should use recusive aria-labelledby properly', function() {
@@ -76,7 +84,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'ARIA Label This is a label');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'ARIA Label This is a label');
});
it('should include hidden text referred to with aria-labelledby', function () {
@@ -88,7 +96,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is a hidden secret');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is a hidden secret');
});
it('should allow setting the initial inLabelledbyContext value', function () {
@@ -96,8 +104,8 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#lbl1')[0];
- assert.equal(axe.commons.text.accessibleText(target, false), '');
- assert.equal(axe.commons.text.accessibleText(target, true), 'hidden label');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target, false), '');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target, true), 'hidden label');
});
it('should use aria-label if present with no labelledby', function() {
@@ -109,7 +117,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'ARIA Label');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'ARIA Label');
});
it('should use alt on imgs with no ARIA', function() {
@@ -122,7 +130,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#target')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Alt text goes here');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Alt text goes here');
});
it('should use alt on image inputs with no ARIA', function() {
@@ -135,7 +143,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#target')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Alt text goes here');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Alt text goes here');
});
it('should use not use alt on text inputs with no ARIA', function() {
@@ -148,7 +156,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#target')[0];
- assert.equal(axe.commons.text.accessibleText(target), '');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), '');
});
it('should use HTML label if no ARIA information', function() {
@@ -160,7 +168,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'HTML Label');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'HTML Label');
});
it('should handle last ditch title attribute', function() {
@@ -172,7 +180,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is This is a label of italics');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is This is a label of italics');
});
it('should handle totally empty elements', function() {
@@ -184,7 +192,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is This is a label of');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is This is a label of');
});
@@ -197,7 +205,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is This is a label of');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is This is a label of');
});
it('should only show each node once when label is before input', function() {
@@ -205,7 +213,7 @@ describe('text.accessibleText', function() {
'
';
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#target')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'My form input');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'My form input');
});
it('should only show each node once when label follows input', function() {
@@ -214,7 +222,7 @@ describe('text.accessibleText', function() {
'';
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#target')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'My form input');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'My form input');
});
it('should handle nested inputs in normal context', function() {
@@ -226,7 +234,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is This is a label of everything');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is This is a label of everything');
});
it('should use handle nested inputs properly in labelledby context', function() {
@@ -238,7 +246,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is the value of everything');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is the value of everything');
});
it('should use ignore hidden inputs', function() {
@@ -250,7 +258,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is of everything');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is of everything');
});
it('should use handle inputs with no type as if they were text inputs', function() {
@@ -262,7 +270,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is the value of everything');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is the value of everything');
});
it('should use handle nested selects properly in labelledby context', function() {
@@ -276,7 +284,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is first third of everything');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is first third of everything');
});
it('should use handle nested textareas properly in labelledby context', function() {
@@ -288,7 +296,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This is the value of everything');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This is the value of everything');
});
it('should use handle ARIA labels properly in labelledby context', function() {
@@ -301,7 +309,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'This not a span is the value of everything');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'This not a span is the value of everything');
});
it('shoud properly fall back to title', function() {
@@ -309,7 +317,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'a')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Hello');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello');
});
it('should give text even for role=presentation on anchors', function() {
@@ -317,7 +325,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'a')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Hello');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello');
});
it('should give text even for role=presentation on buttons', function() {
@@ -325,21 +333,21 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'button')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Hello');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello');
});
it('should give text even for role=presentation on summary', function() {
fixture.innerHTML = 'Hello';
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'summary')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Hello');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello');
});
it('shoud properly fall back to title', function() {
fixture.innerHTML = '';
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'a')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Hello');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello');
});
it('should give text even for role=none on anchors', function() {
@@ -347,7 +355,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'a')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Hello');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello');
});
it('should give text even for role=none on buttons', function() {
@@ -355,7 +363,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'button')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Hello');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello');
});
it('should give text even for role=none on summary', function() {
@@ -363,7 +371,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'summary')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Hello');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello');
});
it('should not add extra spaces around phrasing elements', function() {
@@ -371,7 +379,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'a')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'HelloWorld');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'HelloWorld');
});
it('should add spaces around non-phrasing elements', function() {
@@ -379,7 +387,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'a')[0];
- assert.equal(axe.commons.text.accessibleText(target), 'Hello World');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Hello World');
});
it('should not look at scripts', function() {
@@ -387,7 +395,7 @@ describe('text.accessibleText', function() {
axe._tree = axe.utils.getFlattenedTree(fixture);
var target = axe.utils.querySelectorAll(axe._tree, 'a')[0];
- assert.equal(axe.commons.text.accessibleText(target), '');
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), '');
});
it('should use
Nothing here.
';
+ var node = fixture.querySelector('#target');
+ assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ });
+
it('should pass one indirectly aria-owned child when one required', function () {
var params = checkSetup('
Nothing here.
');
assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
From 311661cb8698bb792e41759b1c9f7ff4169bbe16 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Wed, 9 Aug 2017 09:43:41 -0700
Subject: [PATCH 111/142] test: fix aria/required-children for Shadow DOM
---
lib/checks/aria/required-children.js | 2 +-
test/checks/aria/required-children.js | 5 ++---
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/lib/checks/aria/required-children.js b/lib/checks/aria/required-children.js
index be6d50702b..7453eee32d 100644
--- a/lib/checks/aria/required-children.js
+++ b/lib/checks/aria/required-children.js
@@ -31,7 +31,7 @@ function ariaOwns(nodes, role) {
}
function missingRequiredChildren(node, childRoles, all) {
-
+ //jshint maxstatements: 19
var i,
l = childRoles.length,
missing = [],
diff --git a/test/checks/aria/required-children.js b/test/checks/aria/required-children.js
index 6938638c0f..c522b9f4cf 100644
--- a/test/checks/aria/required-children.js
+++ b/test/checks/aria/required-children.js
@@ -98,9 +98,8 @@ describe('aria-required-children', function () {
});
it('should pass a native input with role comboxbox when missing child is role textbox', function () {
- fixture.innerHTML = '
Nothing here.
';
- var node = fixture.querySelector('#target');
- assert.isTrue(checks['aria-required-children'].evaluate.call(checkContext, node));
+ var params = checkSetup('
Nothing here.
');
+ assert.isTrue(checks['aria-required-children'].evaluate.apply(checkContext, params));
});
it('should pass one indirectly aria-owned child when one required', function () {
From 9f7da56f6a9adb311265beac68ff45fc87c9beea Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Wed, 9 Aug 2017 10:43:56 +0200
Subject: [PATCH 112/142] feat: Add new ARIA 1.1 values for haspopup
---
lib/commons/aria/index.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js
index 3a67810fe3..6d3fa943ea 100644
--- a/lib/commons/aria/index.js
+++ b/lib/commons/aria/index.js
@@ -60,8 +60,8 @@ lookupTables.attributes = {
values: ['true', 'false', 'undefined']
},
'aria-haspopup': {
- type: 'boolean',
- values: ['true', 'false']
+ type: 'nmtoken',
+ values: ['true', 'false', 'menu', 'listbox', 'tree', 'grid', 'dialog']
},
'aria-hidden': {
type: 'boolean',
From bb07c2d88d5e7251c347f4e6f00c871db13ae2ae Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Wed, 9 Aug 2017 10:49:58 +0200
Subject: [PATCH 113/142] feat: Add aria-orientation to additional roles
---
lib/commons/aria/index.js | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js
index 6d3fa943ea..b7a8423ae6 100644
--- a/lib/commons/aria/index.js
+++ b/lib/commons/aria/index.js
@@ -261,7 +261,7 @@ lookupTables.role = {
'combobox': {
type: 'composite',
attributes: {
- allowed: ['aria-expanded', 'aria-autocomplete', 'aria-required', 'aria-activedescendant']
+ allowed: ['aria-expanded', 'aria-autocomplete', 'aria-required', 'aria-activedescendant', 'aria-orientation']
},
owned: {
all: ['listbox', 'textbox']
@@ -443,7 +443,7 @@ lookupTables.role = {
'listbox': {
type: 'composite',
attributes: {
- allowed: ['aria-activedescendant', 'aria-multiselectable', 'aria-required', 'aria-expanded']
+ allowed: ['aria-activedescendant', 'aria-multiselectable', 'aria-required', 'aria-expanded', 'aria-orientation']
},
owned: {
all: ['option']
@@ -503,7 +503,7 @@ lookupTables.role = {
'menu': {
type: 'composite',
attributes: {
- allowed: ['aria-activedescendant', 'aria-expanded']
+ allowed: ['aria-activedescendant', 'aria-expanded', 'aria-orientation']
},
owned: {
one: ['menuitem', 'menuitemradio', 'menuitemcheckbox']
@@ -515,7 +515,7 @@ lookupTables.role = {
'menubar': {
type: 'composite',
attributes: {
- allowed: ['aria-activedescendant', 'aria-expanded']
+ allowed: ['aria-activedescendant', 'aria-expanded', 'aria-orientation']
},
owned: null,
nameFrom: ['author'],
@@ -794,7 +794,7 @@ lookupTables.role = {
'tablist': {
type: 'composite',
attributes: {
- allowed: ['aria-activedescendant', 'aria-expanded', 'aria-level', 'aria-multiselectable']
+ allowed: ['aria-activedescendant', 'aria-expanded', 'aria-level', 'aria-multiselectable', 'aria-orientation']
},
owned: {
all: ['tab']
@@ -870,7 +870,7 @@ lookupTables.role = {
'tree': {
type: 'composite',
attributes: {
- allowed: ['aria-activedescendant', 'aria-multiselectable', 'aria-required', 'aria-expanded']
+ allowed: ['aria-activedescendant', 'aria-multiselectable', 'aria-required', 'aria-expanded', 'aria-orientation']
},
owned: {
all: ['treeitem']
@@ -882,7 +882,7 @@ lookupTables.role = {
type: 'composite',
attributes: {
allowed: ['aria-activedescendant', 'aria-colcount', 'aria-expanded', 'aria-level', 'aria-multiselectable',
- 'aria-readonly', 'aria-required', 'aria-rowcount']
+ 'aria-readonly', 'aria-required', 'aria-rowcount', 'aria-orientation']
},
owned: {
all: ['treeitem']
From 645d1fa52f1bee7e452a3d9b5927b32005fa5a93 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Wed, 9 Aug 2017 10:51:28 +0200
Subject: [PATCH 114/142] fix(aria): Treegrid should own rows, not treeitems
---
lib/commons/aria/index.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js
index b7a8423ae6..2e844d8b14 100644
--- a/lib/commons/aria/index.js
+++ b/lib/commons/aria/index.js
@@ -885,7 +885,7 @@ lookupTables.role = {
'aria-readonly', 'aria-required', 'aria-rowcount', 'aria-orientation']
},
owned: {
- all: ['treeitem']
+ one: ['rowgroup', 'row']
},
nameFrom: ['author'],
context: null
@@ -897,7 +897,7 @@ lookupTables.role = {
},
owned: null,
nameFrom: ['author', 'contents'],
- context: ['treegrid', 'tree']
+ context: ['group', 'tree']
},
'widget': {
type: 'abstract'
From c8aca4f2a3eb18fdb6277d24bb5c5bc3b057f40d Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Wed, 9 Aug 2017 10:07:40 -0700
Subject: [PATCH 115/142] chore: update changelog for 3.0.0-alpha-1
---
CHANGELOG | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
index 209ee7e6d6..abcab7f7d9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -110,11 +110,12 @@ v3.0.0-alpha-1
- All commons and utilities refactored to support Shadow DOM
- Tests all refactored to support Shadow DOM
- API docs and developer guide now cover developing for Shadow DOM
- - Fix for impact of incomplete nodes
+ - Fix for impact of incomplete nodes (#356)
+ - Fix for combobox false positive (#160)
additions:
- Commons virtual methods for handling virtual DOM
- New axe.utils.querySelectorAll method for querying the flattened tree
- New core APIs and test utilities for Shadow DOM development
- Support for ARIA 1.1: role=feed, role=term, aria-placeholder, aria-modal,
- aria-current, aria-keyshortcuts
+ aria-current, aria-keyshortcuts, role=treegrid, aria-orientation, aria-haspopup
From fe9c5ef70aac8629f56cea68144d70a906260633 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Thu, 10 Aug 2017 11:15:35 -0700
Subject: [PATCH 116/142] chore: fix version for 3.0.0-alpha.1
---
CHANGELOG | 2 +-
package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
index abcab7f7d9..03f254cf71 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -103,7 +103,7 @@ v2.3.1:
changes:
- Improvements to hidden-content rule
- Deduplicated langs in valid-lang options
-v3.0.0-alpha-1
+v3.0.0-alpha.1
date: 2017-08-08
changes:
- Shadow DOM support: rules and checks now supply a `virtualNode` in addition to the light DOM `node`. No styling support until supported by Chrome
diff --git a/package.json b/package.json
index 274f2d793a..ecb8b05c19 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "axe-core",
"description": "Accessibility engine for automated Web UI testing",
- "version": "3.0.0-alpha-1",
+ "version": "3.0.0-alpha.1",
"license": "MPL-2.0",
"contributors": [
{
From 25ddb47ec4eec565da330558ee061fd6e34a7c24 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Fri, 11 Aug 2017 12:39:46 +0200
Subject: [PATCH 117/142] feat: Add sri-history file and update process (#476)
---
build/sri-update.js | 47 +++++++++++++++++++++++++++++++++++++++
package.json | 9 +++++---
sri-history.json | 54 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 107 insertions(+), 3 deletions(-)
create mode 100644 build/sri-update.js
create mode 100644 sri-history.json
diff --git a/build/sri-update.js b/build/sri-update.js
new file mode 100644
index 0000000000..b88c97cdf9
--- /dev/null
+++ b/build/sri-update.js
@@ -0,0 +1,47 @@
+var path = require('path');
+var fs = require('fs');
+var sriToolbox = require("sri-toolbox");
+
+// Check if we should be validating or updating
+var validate = process.argv.some(function (arg) {
+ return arg === '--validate';
+});
+
+var root = path.join(__dirname, '..')
+var axeVersion = require('../package.json').version;
+var axeHistory = require('../sri-history.json');
+
+if (typeof axeHistory[axeVersion] !== 'object') {
+ axeHistory[axeVersion] = {}
+}
+var versionSRIs = axeHistory[axeVersion];
+
+// List all axe files (including minified and localized axe files)
+var axeFiles = fs.readdirSync(root).filter(function (file) {
+ return file.match(/^axe(\.[a-z.-]+)?\.js$/);
+});
+
+axeFiles.forEach(function (axeFile) {
+ var axeSource = fs.readFileSync(path.join(root, axeFile), 'utf-8');
+ var axeIntegrity = sriToolbox.generate({ algorithms: ["sha256"] }, axeSource);
+
+ if (!validate) {
+ // Update SRI
+ versionSRIs[axeFile] = axeIntegrity;
+
+ // Test if the SRI shouldn't be changed
+ } else if (versionSRIs[axeFile] && versionSRIs[axeFile] !== axeIntegrity) {
+ console.log(axeFile, 'did not match the SRI in sri-history.json')
+ process.exitCode = 1;
+ }
+});
+
+if (!validate) {
+ fs.writeFileSync(path.join(root, './sri-history.json'),
+ JSON.stringify(axeHistory, null, '\t'), 'utf-8');
+ console.log("Updated sri-history.json ")
+
+} else if (process.exitCode === 1) {
+ console.log('\nMake sure the package version and sri-history.json is updated ' +
+ 'before publishing to NPM.\n');
+}
diff --git a/package.json b/package.json
index ecb8b05c19..6c7dcef9b3 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,8 @@
"build": "grunt",
"test": "grunt test",
"test-fast": "grunt test-fast",
- "prepublishOnly": "grunt build"
+ "version": "node build/sri-update",
+ "prepublishOnly": "grunt build && node build/sri-update --validate"
},
"devDependencies": {
"babel-plugin-transform-object-rest-spread": "^6.6.5",
@@ -79,6 +80,8 @@
"mocha": "^3.2.0",
"promise": "~7.1.1",
"revalidator": "~0.3.1",
- "selenium-webdriver": "~3.4.0"
- }
+ "selenium-webdriver": "~3.4.0",
+ "sri-toolbox": "^0.2.0"
+ },
+ "dependencies": {}
}
diff --git a/sri-history.json b/sri-history.json
new file mode 100644
index 0000000000..7bc1eeeb04
--- /dev/null
+++ b/sri-history.json
@@ -0,0 +1,54 @@
+{
+ "1.0.1": {
+ "axe.js": "sha256-F14wfpbaL6+ZafS2ufmz74/R6CD1L777gCOJVuLn5Ao=",
+ "axe.min.js": "sha256-GfbQgd6aKJKQT21HaQfVUuGEbMUfSKhi7RsWrrih7SM="
+ },
+ "1.1.0": {
+ "axe.js": "sha256-nG9+LLksY/HS6yG4x0iS08U7RAmTucy50uXQS5ndQnM=",
+ "axe.min.js": "sha256-5Lfk3/s+ieHttrDTOCVuepGjDgH2D1neTDDtXnJdIT0="
+ },
+ "1.1.1": {
+ "axe.js": "sha256-K0MLUClls79s14CT01F82LPc+ZubTBwt6fZDdN5iyFw=",
+ "axe.min.js": "sha256-UWXq259l36QypXwd2e4K2V1Lli2qHeSPmKfDXy+7uIM="
+ },
+ "2.0.5": {
+ "axe.js": "sha256-KVA5sj4tNmFLneuHPPbg4iEp3MBzsHvn3+s9CxfMrmc=",
+ "axe.min.js": "sha256-lt5eNq/L7IBUaoSUSAbQ7MEO02DThMGdVm/oxeA88gk="
+ },
+ "2.0.6": {
+ "axe.js": "sha256-cLV1ABoE5NjfwLaRAJYIstAJCmciDXZ55/TrQS5rR/M=",
+ "axe.min.js": "sha256-fnbwW70tA7/ya+5q5Oimc8wCRsdiv3WrU0MElp/euvY="
+ },
+ "2.0.7": {
+ "axe.js": "sha256-HjHe1xKQqP6i3eGpDlARb5HkFsZxvAslvr1JJhUlp60=",
+ "axe.min.js": "sha256-vo7Qs4YxFxzFEW5DG9u05JdAUCsFxx7dBIoBP5rzmKA="
+ },
+ "2.1.7": {
+ "axe.js": "sha256-kXUowSb3HQy6AChF4LYR4cvNxKEHCWqCcuiSxvY6E1s=",
+ "axe.min.js": "sha256-xJPQwkKDFmgwYDLqrt/esIHlc2HLUB2ogf9c1uS1BXA="
+ },
+ "2.2.0": {
+ "axe.js": "sha256-0iX4Q9QT3uHAo+oxS5NWsrU4DNZKB30JRf7m+jjZcX0=",
+ "axe.min.js": "sha256-3UsQJdfWbhtVd0QzcYi2VAJEfs8DMzxrRvr3h9WbMiE="
+ },
+ "2.2.1": {
+ "axe.js": "sha256-1kuZOWtoYszbvrtG7TYfl/FuO48uwZeFpN4aNnebABk=",
+ "axe.min.js": "sha256-WSHVQ1/IJaG9ZOydoFg+QXHLjOq8x/mhYkRAyN1yAdI="
+ },
+ "2.2.2": {
+ "axe.js": "sha256-jTirRblNTKinTlmCK3FqDG6POtZVbP/bJNdrXZhCuqk=",
+ "axe.min.js": "sha256-zpmYSLTgHx09UyYIvYrS5K6uj4VuBbspk54kHk5hPqo="
+ },
+ "2.2.3": {
+ "axe.js": "sha256-dV8RkE0hyyzj7qgValVkoP7Rtu8789BmbyeHZmidvqA=",
+ "axe.min.js": "sha256-BrNWjOcDL265Me8eKQosGWerJ6ju2g+G9+RvMWiBGOA="
+ },
+ "2.3.0": {
+ "axe.js": "sha256-rkyHB2lHjs+tissQLBUxuxIvWlzRbS4f4cdaH+TjQvo=",
+ "axe.min.js": "sha256-MGWkallV18uw6bSq6w8cjbGsf9v4rJtXP+NDtMEbO14="
+ },
+ "2.3.1": {
+ "axe.js": "sha256-63oq1xHBiOhX9jlvn5sJmoL8TwJ8JaLYIB91gyb74RI=",
+ "axe.min.js": "sha256-BGAllCBTUJjJXw3yOPMVai2Bj+1PVaEhK2na699nI/o="
+ }
+}
\ No newline at end of file
From fd607ddf0bfc9536b77017cee3a16c99f13c1c24 Mon Sep 17 00:00:00 2001
From: Dylan Barrell
Date: Wed, 16 Aug 2017 13:33:16 -0400
Subject: [PATCH 118/142] Add University of Nebraska to the projects list
---
doc/projects.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/doc/projects.md b/doc/projects.md
index 15b10c33a3..d2248173a2 100644
--- a/doc/projects.md
+++ b/doc/projects.md
@@ -30,3 +30,4 @@ Add your project/integration to this file and submit a pull request.
1. [Rocket Validator](https://rocketvalidator.com)
1. [aXe Reports](https://github.com/louis-reed/axe-reports)
1. [aXe-TestCafe](https://github.com/helen-dikareva/axe-testcafe)
+1. [Web Audit University of Nebraska-Lincoln](https://webaudit.unl.edu/)
From 277fdb3c66b8a069ca043233c567d9e70c8c3d88 Mon Sep 17 00:00:00 2001
From: Romeeka Gayhart
Date: Fri, 18 Aug 2017 03:43:37 -0700
Subject: [PATCH 119/142] docs: Minor spelling fix (#490)
correct misspelling in CONTRIBUTING doc
---
CONTRIBUTING.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1e5a0b49d0..6ed34eb678 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,7 @@
## Contributor License Agreement
-In order to contribute, you must accept the [contributor licence agreement](https://cla-assistant.io/dequelabs/axe-core) (CLA). Acceptance of this agreement will be checked automatically and pull requests without a CLA cannot be merged.
+In order to contribute, you must accept the [contributor license agreement](https://cla-assistant.io/dequelabs/axe-core) (CLA). Acceptance of this agreement will be checked automatically and pull requests without a CLA cannot be merged.
## Contribution Guidelines
From 41853fea103bb11667560d95e543ee5642c241b8 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Wed, 2 Aug 2017 17:44:17 -0700
Subject: [PATCH 120/142] docs: document help text and incomplete messages
---
doc/rules.md | 66 ++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 56 insertions(+), 10 deletions(-)
diff --git a/doc/rules.md b/doc/rules.md
index 97e5a1ec76..b86f101f91 100644
--- a/doc/rules.md
+++ b/doc/rules.md
@@ -31,16 +31,62 @@ The actual testing of elements in axe-core is done by checks. A rule has one or
| enabled | Does the rule run by default
| tags | Grouping for the rule, such as wcag2a, best-practice
| metadata.description | Description of what a rule does
-| metadata.help | Description of how to resolve an issue
+| metadata.help | Short description of a violation, used in the aXe extension sidebar
## Check Properties
-| Prop. Name | Description
-|-----------------|-----------------
-| id | Unique identifier for the check
-| evaluate | Evaluating function, returning a boolean value
-| options | Configurable value for the check
-| after | Cleanup function, run after check is done
-| metadata impact | "minor", "serious", "critical"
-| metadata pass | Describes why the check passed
-| metadata fail | Describes why the check failed
+| Prop. Name | Description
+|--------------------|-----------------
+| id | Unique identifier for the check
+| evaluate | Evaluating function, returning a boolean value
+| options | Configurable value for the check
+| after | Cleanup function, run after check is done
+| metadata impact | "minor", "serious", "critical"
+| metadata pass | Describes why the check passed
+| metadata fail | Describes why the check failed
+| metadata incomplete| Describes why the check returned undefined
+
+Incomplete results occur when axe-core can’t produce a clear pass or fail result,
+giving users the opportunity to review it manually. Incomplete messages can take
+the form of a string, or an object with arbitrary keys matching the data returned
+from the check.
+
+Incomplete is optional for checks, while pass and fail are required.
+
+### Incomplete message string
+
+As one example, the audio and video caption checks return an incomplete string:
+```
+messages: {
+ pass: 'Why the check passed',
+ fail: 'Why the check failed',
+ incomplete: 'Why the check returned undefined'
+}
+```
+
+### Incomplete message object with missingData
+
+As another example, the color-contrast check returns missingData to aid in
+remediation. Here’s the message format:
+
+```
+messages: {
+ pass: 'Why the check passed',
+ fail: 'Why the check failed',
+ incomplete: {
+ bgImage: 'The background color could not be determined due to a background image',
+ default: 'fallback string'
+ }
+}
+```
+
+To wire up an incomplete message with a specific reason it returned undefined,
+the check needs matching data. Otherwise, it will fall back to the `default` message.
+Reasons are arbitrary for the check (such as 'bgImage') but they must match the
+data returned:
+
+```
+this.data({
+ missingData: 'bgImage'
+});
+```
From b7b7ebc3ce29174b9c1fbfd077dde3f919f3a719 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Wed, 2 Aug 2017 18:01:32 -0700
Subject: [PATCH 121/142] docs: add incomplete to developer guide
---
doc/developer-guide.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/doc/developer-guide.md b/doc/developer-guide.md
index 7e0125af99..86cfa0d444 100644
--- a/doc/developer-guide.md
+++ b/doc/developer-guide.md
@@ -99,6 +99,7 @@ Similar to Rules, Checks are defined by JSON files in the [lib/checks directory]
* `messages` - `Object` These messages are displayed when the Check passes or fails
* `pass` - `String` [doT.js](http://olado.github.io/doT/) template string displayed when the Check passes
* `fail` - `String` [doT.js](http://olado.github.io/doT/) template string displayed when the Check fails
+ * `incomplete` – `String` [doT.js](http://olado.github.io/doT/) template string displayed when the Check is incomplete OR `Object` consisting of missingData for why it returned incomplete. Refer to [rules.md](./rules.md).
#### Check `evaluate`
From b2523cd7022ac01ee1c177271b9cb860332c750e Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Mon, 7 Aug 2017 16:36:18 -0700
Subject: [PATCH 122/142] docs: incorporate feedback for incomplete checks
---
doc/rules.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/doc/rules.md b/doc/rules.md
index b86f101f91..53ab960ba2 100644
--- a/doc/rules.md
+++ b/doc/rules.md
@@ -44,14 +44,14 @@ The actual testing of elements in axe-core is done by checks. A rule has one or
| metadata impact | "minor", "serious", "critical"
| metadata pass | Describes why the check passed
| metadata fail | Describes why the check failed
-| metadata incomplete| Describes why the check returned undefined
+| metadata incomplete| Describes why the check didn’t complete
Incomplete results occur when axe-core can’t produce a clear pass or fail result,
giving users the opportunity to review it manually. Incomplete messages can take
the form of a string, or an object with arbitrary keys matching the data returned
from the check.
-Incomplete is optional for checks, while pass and fail are required.
+A pass message is required, while fail and incomplete are dependent on the check result.
### Incomplete message string
From 38ccda28ad32e5f0895e08886c15ba50de9a808e Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Mon, 7 Aug 2017 16:36:36 -0700
Subject: [PATCH 123/142] docs: add pass/fail template example
Closes https://github.com/dequelabs/axe-core/issues/475
---
doc/developer-guide.md | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/doc/developer-guide.md b/doc/developer-guide.md
index 86cfa0d444..083e2d5669 100644
--- a/doc/developer-guide.md
+++ b/doc/developer-guide.md
@@ -99,7 +99,7 @@ Similar to Rules, Checks are defined by JSON files in the [lib/checks directory]
* `messages` - `Object` These messages are displayed when the Check passes or fails
* `pass` - `String` [doT.js](http://olado.github.io/doT/) template string displayed when the Check passes
* `fail` - `String` [doT.js](http://olado.github.io/doT/) template string displayed when the Check fails
- * `incomplete` – `String` [doT.js](http://olado.github.io/doT/) template string displayed when the Check is incomplete OR `Object` consisting of missingData for why it returned incomplete. Refer to [rules.md](./rules.md).
+ * `incomplete` – `String|Object` – [doT.js](http://olado.github.io/doT/) template string displayed when the Check is incomplete OR an object with `missingData` on why it returned incomplete. Refer to [rules.md](./rules.md).
#### Check `evaluate`
@@ -142,6 +142,17 @@ return results.filter(function (r) {
Occasionally, you may want to add additional information about why a Check passed, failed or returned undefined into its message. For example, the [aria-valid-attr](../lib/checks/aria/valid-attr.json) will add information about any invalid ARIA attributes to its fail message. The message uses the [doT.js](http://olado.github.io/doT/) and is compiled to a JavaScript function at build-time. In the Check message, you have access to the `CheckResult` as `it`.
+```javascript
+// aria-valid-attr check
+"messages": {
+ "pass": "ARIA attributes are used correctly for the defined role",
+ "fail": "ARIA attribute{{=it.data && it.data.length > 1 ? 's are' : ' is'}} not allowed:{{~it.data:value}} {{=value}}{{~}}"
+}
+```
+
+See [rules.md](./rules.md) for more information on writing rules and checks,
+including incomplete results.
+
#### CheckResult
When a Check is executed, its result is then added to a [CheckResult object](../lib/core/base/check-result.js). Much like the RuleResult object, the CheckResult object only contains information that is required to determine whether a Check, and its parent Rule passed or failed. `metadata` from the originating Check is combined later and will not be available until aXe reaches the reporting stage.
From 237ccff2b10f54649a2e065a8ec6d38a54aeca54 Mon Sep 17 00:00:00 2001
From: Brennan Payne
Date: Fri, 11 Aug 2017 12:18:16 -0500
Subject: [PATCH 124/142] Update API.md - Fix Typo
---
doc/API.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/API.md b/doc/API.md
index bb7efc8eda..11bfd3d3a1 100644
--- a/doc/API.md
+++ b/doc/API.md
@@ -330,7 +330,7 @@ Additionally, there are a number or properties that allow configuration of diffe
|-----------------|:-------:|:----------------------------:|
| `runOnly` | n/a | Limit which rules are executed, based on names or tags
| `rules` | n/a | Allow customizing a rule's properties (including { enable: false })
-| `reporter` | `v1` | Which reporter to use (see [Configutration](#api-name-axeconfigure))
+| `reporter` | `v1` | Which reporter to use (see [Configuration](#api-name-axeconfigure))
| `xpath` | `false` | Return xpath selectors for elements
| `absolutePaths` | `false` | Use absolute paths when creating element selectors
| `iframes` | `true` | Tell axe to run inside iframes
From 9b3d19910ac0c61c064311881d589e64e54c0a56 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Tue, 15 Aug 2017 11:32:51 -0700
Subject: [PATCH 125/142] feat: add precommit hook on npm postinstall
---
package.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 6c7dcef9b3..f37278c216 100644
--- a/package.json
+++ b/package.json
@@ -51,9 +51,11 @@
"test": "grunt test",
"test-fast": "grunt test-fast",
"version": "node build/sri-update",
- "prepublishOnly": "grunt build && node build/sri-update --validate"
+ "prepublishOnly": "grunt build && node build/sri-update --validate",
+ "postinstall": "ln -s -f node_modules/angular-precommit/index.js .git/hooks/commit-msg"
},
"devDependencies": {
+ "angular-precommit": "^1.0.3",
"babel-plugin-transform-object-rest-spread": "^6.6.5",
"babel-polyfill": "^6.7.4",
"babel-preset-es2015": "^6.6.0",
From 16f2f76cac9282e35e2ff18e4b0353b0d942ec6f Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Tue, 15 Aug 2017 12:48:46 -0700
Subject: [PATCH 126/142] fix: copy precommit hook as file, not a link
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index f37278c216..66b84a9f1a 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,7 @@
"test-fast": "grunt test-fast",
"version": "node build/sri-update",
"prepublishOnly": "grunt build && node build/sri-update --validate",
- "postinstall": "ln -s -f node_modules/angular-precommit/index.js .git/hooks/commit-msg"
+ "postinstall": "cd .git/hooks/ && cp ../../node_modules/angular-precommit/index.js commit-msg && cd ../../"
},
"devDependencies": {
"angular-precommit": "^1.0.3",
From 1107783124c5d7c2195e2fedb005558208e8db18 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Tue, 15 Aug 2017 13:14:04 -0700
Subject: [PATCH 127/142] fix: only run postinstall if .git exists
---
bin/postinstall.sh | 8 ++++++++
package.json | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
create mode 100755 bin/postinstall.sh
diff --git a/bin/postinstall.sh b/bin/postinstall.sh
new file mode 100755
index 0000000000..d6a0fb45b0
--- /dev/null
+++ b/bin/postinstall.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if [ -d ".git" ]; then
+ if [ ! -f ".git/hooks/commit-msg" ]; then
+ echo "Installing pre-commit hook"
+ cd .git/hooks/ && cp ../../node_modules/angular-precommit/index.js commit-msg && cd ../../
+ fi
+fi
\ No newline at end of file
diff --git a/package.json b/package.json
index 66b84a9f1a..68bd753d8b 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,7 @@
"test-fast": "grunt test-fast",
"version": "node build/sri-update",
"prepublishOnly": "grunt build && node build/sri-update --validate",
- "postinstall": "cd .git/hooks/ && cp ../../node_modules/angular-precommit/index.js commit-msg && cd ../../"
+ "postinstall": "./bin/postinstall.sh"
},
"devDependencies": {
"angular-precommit": "^1.0.3",
From 674408f16e58f1f4fb712ea55f00348ecf178cad Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Tue, 15 Aug 2017 13:25:03 -0700
Subject: [PATCH 128/142] fix: try telling circle to skip .git/hooks
---
bin/postinstall.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bin/postinstall.sh b/bin/postinstall.sh
index d6a0fb45b0..72db42e15a 100755
--- a/bin/postinstall.sh
+++ b/bin/postinstall.sh
@@ -1,6 +1,6 @@
#!/bin/sh
-if [ -d ".git" ]; then
+if [ -d ".git/hooks" ]; then
if [ ! -f ".git/hooks/commit-msg" ]; then
echo "Installing pre-commit hook"
cd .git/hooks/ && cp ../../node_modules/angular-precommit/index.js commit-msg && cd ../../
From 508fae216232b9e7bc1981ecbeea196d4704da3a Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Mon, 21 Aug 2017 02:56:41 -0700
Subject: [PATCH 129/142] feat: allow link text from single-cell layout table
(#495)
Closes https://dequesrc.atlassian.net/browse/WWD-547
---
lib/commons/text/accessible-text-virtual.js | 12 ++++++++++--
test/commons/text/accessible-text.js | 16 ++++++++++++++++
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/lib/commons/text/accessible-text-virtual.js b/lib/commons/text/accessible-text-virtual.js
index db9ce355d4..002a313f8a 100644
--- a/lib/commons/text/accessible-text-virtual.js
+++ b/lib/commons/text/accessible-text-virtual.js
@@ -169,8 +169,16 @@ text.accessibleTextVirtual = function accessibleTextVirtual(element, inLabelledB
}, '');
}
+ function getLayoutTableText (element) {
+ // // check if layout table only has one cell
+ if (!axe.commons.table.isDataTable(element.actualNode) && axe.commons.table.getAllCells(element.actualNode).length === 1) {
+ return getInnerText(element, false, false).trim();
+ }
+ return '';
+ }
+
function checkNative (element, inLabelledByContext, inControlContext) {
- // jshint maxstatements:30
+ // jshint maxstatements:30, maxcomplexity: 20
let returnText = '';
const { actualNode } = element;
const nodeName = actualNode.nodeName.toUpperCase();
@@ -197,7 +205,7 @@ text.accessibleTextVirtual = function accessibleTextVirtual(element, inLabelledB
}
returnText = (actualNode.getAttribute('title') ||
- actualNode.getAttribute('summary') || '');
+ actualNode.getAttribute('summary') || getLayoutTableText(element) || '');
if (nonEmptyText(returnText)) {
return returnText;
diff --git a/test/commons/text/accessible-text.js b/test/commons/text/accessible-text.js
index 84c0ea89f9..d69f7faa86 100644
--- a/test/commons/text/accessible-text.js
+++ b/test/commons/text/accessible-text.js
@@ -911,6 +911,22 @@ describe('text.accessibleTextVirtual', function() {
var target = axe.utils.querySelectorAll(axe._tree, 'a')[0];
assert.equal(axe.commons.text.accessibleTextVirtual(target), '');
});
+
+ it('should use text from a table with a single cell and role=presentation', function() {
+ fixture.innerHTML = '' +
+ '
' +
+ '
' +
+ '
' +
+ 'Descriptive Link Text' +
+ '
' +
+ '
' +
+ '
' +
+ '';
+ axe._tree = axe.utils.getFlattenedTree(fixture);
+
+ var target = axe.utils.querySelectorAll(axe._tree, 'a')[0];
+ assert.equal(axe.commons.text.accessibleTextVirtual(target), 'Descriptive Link Text');
+ });
});
describe('button', function() {
From 94008ff572d49b13489dc964194a8cda4b686396 Mon Sep 17 00:00:00 2001
From: Matt Isner
Date: Tue, 22 Aug 2017 04:53:40 -0400
Subject: [PATCH 130/142] fix: Use frame query that supports shadow dom (#492)
---
lib/core/public/cleanup-plugins.js | 6 ++++--
lib/core/public/plugins.js | 2 +-
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/lib/core/public/cleanup-plugins.js b/lib/core/public/cleanup-plugins.js
index 46b3fab118..a29f9cbca0 100644
--- a/lib/core/public/cleanup-plugins.js
+++ b/lib/core/public/cleanup-plugins.js
@@ -23,9 +23,11 @@ function cleanupPlugins(resolve, reject) {
});
});
- axe.utils.toArray(document.querySelectorAll('frame, iframe')).forEach(function (frame) {
+ var flattenedTree = axe.utils.getFlattenedTree(document.body)
+
+ axe.utils.querySelectorAll(flattenedTree, 'iframe, frame').forEach(function (node) {
q.defer(function (res, rej) {
- return axe.utils.sendCommandToFrame(frame, {
+ return axe.utils.sendCommandToFrame(node.actualNode, {
command: 'cleanup-plugin'
}, res, rej);
});
diff --git a/lib/core/public/plugins.js b/lib/core/public/plugins.js
index 65ba6253c1..ba71f9e40a 100644
--- a/lib/core/public/plugins.js
+++ b/lib/core/public/plugins.js
@@ -43,4 +43,4 @@ Plugin.prototype.add = function (impl) {
axe.registerPlugin = function (plugin) {
'use strict';
axe.plugins[plugin.id] = new Plugin(plugin);
-};
\ No newline at end of file
+};
From 0467fa7d24391157ab28d6ba61a14b1a61095c72 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Tue, 22 Aug 2017 16:09:21 +0200
Subject: [PATCH 131/142] chore: Fix lint problem in cleanup-plugin
---
lib/core/public/cleanup-plugins.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/core/public/cleanup-plugins.js b/lib/core/public/cleanup-plugins.js
index a29f9cbca0..b3e6a0e876 100644
--- a/lib/core/public/cleanup-plugins.js
+++ b/lib/core/public/cleanup-plugins.js
@@ -23,7 +23,7 @@ function cleanupPlugins(resolve, reject) {
});
});
- var flattenedTree = axe.utils.getFlattenedTree(document.body)
+ var flattenedTree = axe.utils.getFlattenedTree(document.body);
axe.utils.querySelectorAll(flattenedTree, 'iframe, frame').forEach(function (node) {
q.defer(function (res, rej) {
From 859f5da926060cfce62992a0444768b4d8139790 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Tue, 29 Aug 2017 00:40:16 -0700
Subject: [PATCH 132/142] chore: remove accesslint from projects (#503)
---
doc/projects.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/doc/projects.md b/doc/projects.md
index d2248173a2..d26e316bc0 100644
--- a/doc/projects.md
+++ b/doc/projects.md
@@ -19,7 +19,6 @@ Add your project/integration to this file and submit a pull request.
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)
From 5a77c2fe14cdb75dcf6deb083c28e08dfdc57472 Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Mon, 28 Aug 2017 15:28:05 -0700
Subject: [PATCH 133/142] fix: expand tr support for color contrast
---
lib/commons/color/get-background-color.js | 22 +++++-----
test/commons/color/get-background-color.js | 48 ++++++++++++++++++++--
2 files changed, 55 insertions(+), 15 deletions(-)
diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js
index 2d35d95eea..47e19e2a93 100644
--- a/lib/commons/color/get-background-color.js
+++ b/lib/commons/color/get-background-color.js
@@ -121,22 +121,22 @@ function elmPartiallyObscured(elm, bgElm, bgColor) {
* @param {Element} elm
*/
function includeMissingElements(elmStack, elm) {
- const elementMap = {'TD': 'TR', 'INPUT': 'LABEL'};
+ const elementMap = {'TD': 'TR', 'TH': 'TR', 'INPUT': 'LABEL'};
const tagArray = elmStack.map((elm) => {
return elm.tagName;
});
let bgNodes = elmStack;
for (let candidate in elementMap) {
- if (elementMap.hasOwnProperty(candidate)) {
- // tagName matches key
- if (elm.tagName === candidate) {
- let ancestorMatch = axe.commons.dom.findUp(elm, elementMap[candidate]);
- if (ancestorMatch && elmStack.indexOf(ancestorMatch) === -1) {
- // found an ancestor not in elmStack, and it overlaps
- let overlaps = axe.commons.dom.visuallyOverlaps(elm.getBoundingClientRect(), ancestorMatch);
- if (overlaps) {
- bgNodes.splice(elmStack.indexOf(elm) + 1, 0, ancestorMatch);
- }
+ // check that TR or LABEL has paired nodeName from elementMap, but don't expect elm to be that candidate
+ if (tagArray.includes(candidate)) {
+ // look up the tree for a matching candidate
+ let ancestorMatch = axe.commons.dom.findUp(elm, elementMap[candidate]);
+ if (ancestorMatch && elmStack.indexOf(ancestorMatch) === -1) {
+ // found an ancestor not in elmStack, and it overlaps
+ let overlaps = axe.commons.dom.visuallyOverlaps(elm.getBoundingClientRect(), ancestorMatch);
+ if (overlaps) {
+ // if target is in the elementMap, use its position.
+ bgNodes.splice(tagArray.indexOf(candidate) + 1, 0, ancestorMatch);
}
}
// tagName matches value
diff --git a/test/commons/color/get-background-color.js b/test/commons/color/get-background-color.js
index 8a62a0264f..35723c2f3b 100644
--- a/test/commons/color/get-background-color.js
+++ b/test/commons/color/get-background-color.js
@@ -192,14 +192,54 @@ describe('color.getBackgroundColor', function () {
assert.deepEqual(bgNodes, [target]);
});
- it('should count a TR as a background element', function () {
+ it('should count a TR as a background element for TD', function () {
fixture.innerHTML = '
' +
'
' +
'
' +
- '
' +
+ '
' +
'Cell content
' +
- '
' +
- '
';
+ '' +
+ '';
+ var target = fixture.querySelector('#target'),
+ parent = fixture.querySelector('#parent');
+ var bgNodes = [];
+ var actual = axe.commons.color.getBackgroundColor(target, bgNodes);
+ var expected = new axe.commons.color.Color(243, 243, 243, 1);
+ assert.equal(actual.red, expected.red);
+ assert.equal(actual.green, expected.green);
+ assert.equal(actual.blue, expected.blue);
+ assert.equal(actual.alpha, expected.alpha);
+ assert.deepEqual(bgNodes, [parent]);
+ });
+
+ it('should count a TR as a background element for TH', function () {
+ fixture.innerHTML = '
' +
+ '
' +
+ '
' +
+ '
' +
+ 'Header content
' +
+ '
' +
+ '
';
+ var target = fixture.querySelector('#target'),
+ parent = fixture.querySelector('#parent');
+ var bgNodes = [];
+ var actual = axe.commons.color.getBackgroundColor(target, bgNodes);
+ var expected = new axe.commons.color.Color(243, 243, 243, 1);
+ assert.equal(actual.red, expected.red);
+ assert.equal(actual.green, expected.green);
+ assert.equal(actual.blue, expected.blue);
+ assert.equal(actual.alpha, expected.alpha);
+ assert.deepEqual(bgNodes, [parent]);
+ });
+
+ it('should count a TR as a background element for a child element', function () {
+ fixture.innerHTML = '
' +
+ '
' +
+ '
' +
+ '
' +
+ 'Cell content' +
+ '
' +
+ '
';
var target = fixture.querySelector('#target'),
parent = fixture.querySelector('#parent');
var bgNodes = [];
From 9402a7c4c05942207fc4f480b3acbbf70df03efa Mon Sep 17 00:00:00 2001
From: Matt Isner
Date: Tue, 22 Aug 2017 11:40:01 -0400
Subject: [PATCH 134/142] chore: ignore node_modules when linting
---
Gruntfile.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Gruntfile.js b/Gruntfile.js
index 54ad938d05..2f28d89f3b 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -329,7 +329,8 @@ module.exports = function (grunt) {
},
src: [
'lib/**/*.js', 'test/**/*.js', 'build/tasks/**/*.js',
- 'doc/**/*.js', '!doc/examples/jest+react/*.js', 'Gruntfile.js'
+ 'doc/**/*.js', '!doc/examples/jest+react/*.js', 'Gruntfile.js',
+ '!**/node_modules/**/*.js'
]
}
}
From 7f66ee86ceca3f97a31578385a6dd11b969f8e12 Mon Sep 17 00:00:00 2001
From: Wilco Fiers
Date: Thu, 31 Aug 2017 15:02:20 +0200
Subject: [PATCH 135/142] fix: Ignore shadowRoots on elements that don't allow
them
---
lib/checks/.DS_Store | Bin 0 -> 8196 bytes
lib/core/utils/flattened-tree.js | 18 ++++++++++--
test/core/utils/flattened-tree.js | 46 ++++++++++++++++++++++++++++++
3 files changed, 62 insertions(+), 2 deletions(-)
create mode 100644 lib/checks/.DS_Store
diff --git a/lib/checks/.DS_Store b/lib/checks/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..6a583cd791c947cc0fa884746b4e83e310416d9d
GIT binary patch
literal 8196
zcmeHMTWl0n7(QPqFl(nUKr3aT*##mHX=JNVE=JhixDy(7X$utB*_}ffna(sjvs<8&
z)EF-$28|joPlgyT0UwM)d@y*wfW`+6Mj`PDFENq$Ii`dftwNm@qS3sC1g63BTC9w2URi!ASqG+
zS?Hef0O^wqWjd52N=jFXsj>%zt_V{MD0k8)g*nM|C`Xi3?hMMEAsiWDh5|V{`6Z<}
zLt4qGju40t7>|J1J{ofN=p(Q9jJ
z4ZWteW^ZP{Hk2E4dO7-v1tAp>=9
z=DTe^lr!quJvT6ImltI`6{D}mb}etH%`3T<-*Ynq{jTtj%)#>_fdNgie@3VPv=a6{DyG*~pUCVa+d&=TDI6Ui`1AGRWVlnB}Fek3}
zSv1@>&k}x?aavQF#yRo$9-(P3a;hy&lMeG2#PzICOEt{UaM5C2&mJH$7=9RZX_Fq`
z>6m%$7*(Z?>rHR(HOZc=}mYArvaFhxQ(@R3Q3~_=rvPEnaomg46k9q7t_BeZyonmL$yX+kM
zntjhMvCHgt_6Pfu{l)%?!Ypnuo`QTMl0H|1zp&I46@jZ0yy@eAA>lE
z!x+W`co@g<1fIkTcnv3T60hSl-oYpM65rxGT);(K!mqfjrdBmd;7y~mRuaFF9Jlw0
zz=I)yk5(pdV#CG_^?LgaPv8sF6m9Eooh^C1dTrYkGK^us)jX8sQ|ywg8)cOw_BB*K
zPvo9EuTf9zBy#eD6EVk_Dz<|rt>-UDBn(XwPB<@GOwB6V5=y(YDWMyaB=LooIGr#i
zONxu;iWXfrs^umS%{6zEc8cV>Xs&A{Eu#%08tscI5a_j5fvEpV)34Y?_KTn>7$&0@
zGm*d&BIj}<=6Y--T5iQQbfXu$u?Kxb%zoIo9|sViO!PcN1bv9;`6wR4Q9Olb@GPFg
z^Ei%|@G@S(t9S!%;w_xQ8N7%0@c}->7dStLsGG+S^_xmWEoX|ZK`(GPf7p)
literal 0
HcmV?d00001
diff --git a/lib/core/utils/flattened-tree.js b/lib/core/utils/flattened-tree.js
index 0c5189f325..ec618bbcb1 100644
--- a/lib/core/utils/flattened-tree.js
+++ b/lib/core/utils/flattened-tree.js
@@ -51,6 +51,20 @@ function getSlotChildren(node) {
return retVal;
}
+const possibleShadowRoots = ['article', 'aside', 'blockquote',
+ 'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+ 'header', 'main', 'nav', 'p', 'section', 'span'];
+axe.utils.isShadowRoot = function isShadowRoot (node) {
+ const nodeName = node.nodeName.toLowerCase();
+ if (node.shadowRoot) {
+ if (/^[a-z][a-z0-9_.-]*-[a-z0-9_.-]*$/.test(nodeName) ||
+ possibleShadowRoots.includes(nodeName)) {
+ return true;
+ }
+ }
+ return false;
+};
+
/**
* Recursvely returns an array of the virtual DOM nodes at this level
* excluding comment nodes and the shadow DOM nodes and
@@ -75,8 +89,8 @@ axe.utils.getFlattenedTree = function (node, shadowId) {
node = node.documentElement;
}
nodeName = node.nodeName.toLowerCase();
- // for some reason Chrome's marquee element has an open shadow DOM
- if (node.shadowRoot && nodeName !== 'marquee') {
+
+ if (axe.utils.isShadowRoot(node)) {
// generate an ID for this shadow root and overwrite the current
// closure shadowId with this value so that it cascades down the tree
retVal = virtualDOMfromNode(node, shadowId);
diff --git a/test/core/utils/flattened-tree.js b/test/core/utils/flattened-tree.js
index 279a4af8a6..4d29057588 100644
--- a/test/core/utils/flattened-tree.js
+++ b/test/core/utils/flattened-tree.js
@@ -66,6 +66,32 @@ function shadowIdAssertions () {
}
+describe('isShadowRoot', function () {
+ 'use strict';
+ var isShadowRoot = axe.utils.isShadowRoot;
+
+ it('returns false if the node has no shadowRoot', function () {
+ assert.isFalse(isShadowRoot({ nodeName: 'DIV', shadowRoot: undefined }));
+ });
+ it('returns true if the native element allows shadow DOM', function () {
+ assert.isTrue(isShadowRoot({ nodeName: 'DIV', shadowRoot: {} }));
+ assert.isTrue(isShadowRoot({ nodeName: 'H1', shadowRoot: {} }));
+ assert.isTrue(isShadowRoot({ nodeName: 'ASIDE', shadowRoot: {} }));
+ });
+ it('returns true if a custom element with shadowRoot', function () {
+ assert.isTrue(isShadowRoot({ nodeName: 'X-BUTTON', shadowRoot: {} }));
+ assert.isTrue(isShadowRoot({ nodeName: 'T1000-SCHWARZENEGGER', shadowRoot: {} }));
+ });
+ it('returns true if an invalid custom element with shadowRoot', function () {
+ assert.isFalse(isShadowRoot({ nodeName: '0-BUZZ', shadowRoot: {} }));
+ assert.isFalse(isShadowRoot({ nodeName: '--ELM--', shadowRoot: {} }));
+ });
+ it('returns false if the native element does not allow shadow DOM', function () {
+ assert.isFalse(isShadowRoot({ nodeName: 'IFRAME', shadowRoot: {} }));
+ assert.isFalse(isShadowRoot({ nodeName: 'STRONG', shadowRoot: {} }));
+ });
+});
+
if (shadowSupport.v0) {
describe('flattened-tree shadow DOM v0', function () {
'use strict';
@@ -154,6 +180,26 @@ if (shadowSupport.v1) {
assert.isTrue(virtualDOM[0].children[2].children[1].children[0].children[0].actualNode.textContent === 'fallback content');
assert.isTrue(virtualDOM[0].children[2].children[1].children[0].children[1].actualNode.nodeName === 'LI');
});
+ it('calls isShadowRoot to identify a shadow root', function () {
+ var isShadowRoot = axe.utils.isShadowRoot;
+ fixture.innerHTML = '';
+ var div = fixture.querySelector('div');
+ var shadowRoot = div.attachShadow({ mode: 'open' });
+ shadowRoot.innerHTML = '
Just a man in the back
';
+
+ // Test without isShqdowRoot overwritten
+ assert.equal(axe.utils.getFlattenedTree(div)[0].children.length, 1);
+
+ var called = false;
+ axe.utils.isShadowRoot = function () {
+ called = true;
+ return false;
+ };
+ // Test with isShadowRoot overwritten
+ assert.equal(axe.utils.getFlattenedTree(div)[0].children.length, 0);
+ assert.isTrue(called);
+ axe.utils.isShadowRoot = isShadowRoot;
+ });
});
describe('flattened-tree shadow DOM v1: boxed slots', function () {
'use strict';
From e1e067d8f4445042360b2bef957037d5cdd0b7db Mon Sep 17 00:00:00 2001
From: Marcy Sutton
Date: Tue, 15 Aug 2017 17:11:55 -0700
Subject: [PATCH 136/142] feat: add standard-version
Add tool for automating version bumps and changelog generation
Closes https://dequesrc.atlassian.net/browse/WWD-819
---
CHANGELOG | 121 --------------------------------
CHANGELOG.md | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++
bower.json | 6 +-
package.json | 6 +-
4 files changed, 200 insertions(+), 126 deletions(-)
delete mode 100644 CHANGELOG
create mode 100644 CHANGELOG.md
diff --git a/CHANGELOG b/CHANGELOG
deleted file mode 100644
index 03f254cf71..0000000000
--- a/CHANGELOG
+++ /dev/null
@@ -1,121 +0,0 @@
-v1.0.1:
- date: 2015-06-10
- changes:
- - Initial public release
-v1.1.1:
- date: 2015-09-04
- changes:
- - Adds Travis hooks
- - Adds Sauce Labs
- - Encodes HTML in descriptions
- - Updates messages and help URLs
-v2.0.0:
- date: 2016-03-01
- changes:
- - Adds support for AMD modules
- - Fixes incompatibility with Webpack
- - Improvements to rules and checks
- - Help urls no longer hard-coded
- - Improved error handling
-v2.0.3:
- date: 2016-04-12
- changes:
- - Security improvements
- - Build includes Babel/ES6
- - Improvements to table rules
- - aXe can be loaded in Node
-v2.0.4:
- date: 2016-04-13
- changes:
- - Improvements to messaging for extensions
-v2.0.5:
- date: 2016-04-20
- changes:
- - Support for UMD pattern
- - Adds 508 tagging for table rules
- - Fixes race condition for iframes
- - Exclude actual nodes from array checking
-v2.0.7:
- date: 2016-09-28
- changes:
- - Add TypeScript definition v1
-v2.1.7:
- date: 2016-12-13
- changes:
- - Add promise-based axe.run API method in favor of axe.a11yCheck
- - Move TypeScript definition to root of project
- - Add Inapplicable and Can't Tell results
- - New rule: frame-title-unique
- - Improvements to table rules: td-has-header, th-has-data-cells
- - Color contrast rule performance improvements using polyfilled elementsFromPoint
- - Add better support for implicit roles
- - DQElement supports xPath
-v2.1.8:
- date: 2017-02-21
- changes:
- - Move from Snyk to Retire.js
- - Make CI run test-fast task instead of parallel
- - Add documentation on writing integration tests and rules
- - Allow a larger list of languages for HTML-valid-lang rule
- - Add support for [role=img] in image-alt rule
- - Fix bug with innerHeight in get-background-color
- - Improve dom.is-offscreen function
- - Integrate optional performance timer
- - Empty include defaults to document
-v2.2.0:
- date: 2017-04-24
- changes:
- - Add configuration options for iframes: false, selectors: false, and elementRef: true
- - Improve color-contrast rule for disabled elements
- - Add webdriver task for testing mobile viewports
- - Improve audio/video captioning rules
- - Improve th-has-data-cells rule
- - Expose incomplete reasons for color contrast rule as part of Needs Review
- - Implement rule groupings as tags
- - Allow building of axe in multiple languages
- - Empty-heading rule has impact: moderate
-v2.2.1:
- date: 2017-05-19
- changes:
- - Remove nodes from the color contrast incompleteData API to avoid circular references
-v2.2.2:
- date: 2017-05-25
- changes:
- - Stabilize incompleteData API for backwards compatibility
- - Change impact of duplicate-id rule to moderate
-v2.2.3:
- date: 2017-06-01
- changes:
- - Removed the disable property from link-in-text-block
-v2.3.0:
- date: 2017-06-14
- changes:
- - Overhaul of selectors API
- - New experimental rule for hidden-content
- - New rule for flagging aria-hidden="true" on document.body
- - Color-contrast rule impact is now serious
- - Color-contrast fixes for implicit labels and TR elements
- - Color-contrast puts 1:1 ratio elements into Needs Review/incomplete
- - List category mappings in docs
- - Update axe.source to work with Firefox webdriver
-v2.3.1:
- date: 2017-06-15
- changes:
- - Improvements to hidden-content rule
- - Deduplicated langs in valid-lang options
-v3.0.0-alpha.1
- date: 2017-08-08
- changes:
- - Shadow DOM support: rules and checks now supply a `virtualNode` in addition to the light DOM `node`. No styling support until supported by Chrome
- - All commons and utilities refactored to support Shadow DOM
- - Tests all refactored to support Shadow DOM
- - API docs and developer guide now cover developing for Shadow DOM
- - Fix for impact of incomplete nodes (#356)
- - Fix for combobox false positive (#160)
- additions:
- - Commons virtual methods for handling virtual DOM
- - New axe.utils.querySelectorAll method for querying the flattened tree
- - New core APIs and test utilities for Shadow DOM development
- - Support for ARIA 1.1: role=feed, role=term, aria-placeholder, aria-modal,
- aria-current, aria-keyshortcuts, role=treegrid, aria-orientation, aria-haspopup
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000..ad04f06f1c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,193 @@
+# Change Log
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+
+# [3.0.0-alpha.1](https://github.com/dequelabs/axe-core/compare/v2.3.1...v3.0.0-alpha.1) (2017-08-16)
+
+### Bug Fixes
+
+* add copyright banner back in to axe.js ([2aac29a](https://github.com/dequelabs/axe-core/commit/2aac29a))
+* Adjust if formatting ([2211d78](https://github.com/dequelabs/axe-core/commit/2211d78))
+* Align impact levels with Deque Way ([28f4477](https://github.com/dequelabs/axe-core/commit/28f4477))
+* Allow