Skip to content

Commit

Permalink
* fix: add shadow dom support to color contrast rule
Browse files Browse the repository at this point in the history
* chore: move isShadowRoot to its own file

* fix: add shadow dom support to color contrast rule

Closes #687

* test(color): add tests, incorporate feedback

* feat: move shadowElementsFromPoint to commons.dom

* test: upgrade to circle 2.0 and try failing tests again
  • Loading branch information
marcysutton authored Feb 7, 2018
1 parent 4114833 commit 0440c9b
Show file tree
Hide file tree
Showing 14 changed files with 800 additions and 341 deletions.
8 changes: 4 additions & 4 deletions lib/commons/color/get-background-color.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function contentOverlapping(targetElement, bgNode) {
// get content box of target element
// check to see if the current bgNode is overlapping
var targetRect = targetElement.getClientRects()[0];
var obscuringElements = document.elementsFromPoint(targetRect.left, targetRect.top);
var obscuringElements = dom.shadowElementsFromPoint(targetRect.left, targetRect.top);
if (obscuringElements) {
for(var i = 0; i < obscuringElements.length; i++) {
if (obscuringElements[i] !== targetElement && obscuringElements[i] === bgNode) {
Expand Down Expand Up @@ -204,7 +204,7 @@ color.getCoords = function(rect) {
return {x, y};
};
/**
* Get elements from point for block and inline elements, excluding line breaks
* Get relevant stacks of block and inline elements, excluding line breaks
* @method getRectStack
* @memberof axe.commons.color
* @instance
Expand All @@ -214,9 +214,9 @@ color.getCoords = function(rect) {
color.getRectStack = function(elm) {
let boundingCoords = color.getCoords(elm.getBoundingClientRect());
if (boundingCoords) {
let boundingStack = dom.shadowElementsFromPoint(boundingCoords.x, boundingCoords.y);
// allows inline elements spanning multiple lines to be evaluated
let rects = Array.from(elm.getClientRects());
let boundingStack = Array.from(document.elementsFromPoint(boundingCoords.x, boundingCoords.y));
if (rects && rects.length > 1) {
let filteredArr = rects.filter((rect) => {
// exclude manual line breaks in Chrome/Safari
Expand All @@ -225,7 +225,7 @@ color.getRectStack = function(elm) {
.map((rect) => {
let coords = color.getCoords(rect);
if (coords) {
return Array.from(document.elementsFromPoint(coords.x, coords.y));
return dom.shadowElementsFromPoint(coords.x, coords.y);
}
});
// add bounding client rect stack for comparison later
Expand Down
28 changes: 28 additions & 0 deletions lib/commons/dom/shadow-elements-from-point.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* global axe, dom */
/**
* Get elements from point across shadow dom boundaries
* @method shadowElementsFromPoint
* @memberof axe.commons.dom
* @instance
* @param {Number} nodeX X coordinates of point
* @param {Number} nodeY Y coordinates of point
* @param {Object} [root] Shadow root or document root
* @return {Array}
*/
dom.shadowElementsFromPoint = function(nodeX, nodeY, root = document) {
return root.elementsFromPoint(nodeX, nodeY)
.reduce((stack, elm) => {
if (axe.utils.isShadowRoot(elm)) {
const shadowStack = dom.shadowElementsFromPoint(nodeX, nodeY, elm.shadowRoot);
stack = stack.concat(shadowStack);
// filter host nodes which get included regardless of overlap
// TODO: refactor multiline overlap checking inside shadow dom
if (stack.length && axe.commons.dom.visuallyContains(stack[0], elm)) {
stack.push(elm);
}
} else {
stack.push(elm);
}
return stack;
}, []);
};
14 changes: 0 additions & 14 deletions lib/core/utils/flattened-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,6 @@ 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 <content> and <slot>
Expand Down
21 changes: 21 additions & 0 deletions lib/core/utils/is-shadow-root.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* global axe */

const possibleShadowRoots = ['article', 'aside', 'blockquote',
'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'header', 'main', 'nav', 'p', 'section', 'span'];
/**
* Test a node to see if it has a spec-conforming shadow root
*
* @param {Node} node The HTML DOM node
* @return {Boolean}
*/
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;
};
14 changes: 14 additions & 0 deletions test/checks/color/color-contrast.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ describe('color-contrast', function () {

var fixture = document.getElementById('fixture');
var fixtureSetup = axe.testUtils.fixtureSetup;
var shadowSupported = axe.testUtils.shadowSupport.v1;
var shadowCheckSetup = axe.testUtils.shadowCheckSetup;

var checkContext = {
_relatedNodes: [],
Expand Down Expand Up @@ -289,4 +291,16 @@ describe('color-contrast', function () {
document.querySelector('#background')
);
});

(shadowSupported ? it : xit)
('returns colors across Shadow DOM boundaries', function () {
var params = shadowCheckSetup(
'<div id="container" style="background-color:black;"></div>',
'<p style="color: #333;" id="target">Text</p>'
);
var container = fixture.querySelector('#container');
var result = checks['color-contrast'].evaluate.apply(checkContext, params);
assert.isFalse(result);
assert.deepEqual(checkContext._relatedNodes, [container]);
});
});
199 changes: 194 additions & 5 deletions test/commons/color/get-background-color.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ describe('color.getBackgroundColor', function () {

var fixture = document.getElementById('fixture');

var shadowSupported = axe.testUtils.shadowSupport.v1;

afterEach(function () {
document.getElementById('fixture').innerHTML = '';
axe.commons.color.incompleteData.clear();
Expand Down Expand Up @@ -233,6 +235,26 @@ describe('color.getBackgroundColor', function () {
assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'elmPartiallyObscuring');
});

it('should return an actual if an absolutely positioned element does not cover background', function () {
fixture.innerHTML = '<div style="background-color:black; height:20px; position:relative;">' +
'<div style="color:#333; position:absolute; top:21px;" id="target">Text</div>' +
'</div>';
var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []);
assert.equal(Math.round(actual.blue), 255);
assert.equal(Math.round(actual.red), 255);
assert.equal(Math.round(actual.green), 255);
});

it('should return null if an absolutely positioned element partially obsures background', function () {
fixture.innerHTML = '<div style="height:40px; position:relative;">' +
'<div style="background-color:black; height:20px;"></div>' +
'<div style="color:#333; position:absolute; margin-top:-11px;" id="target">Text</div>' +
'</div>';
var actual = axe.commons.color.getBackgroundColor(document.getElementById('target'), []);
assert.isNull(actual);
assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'elmPartiallyObscured');
});

it('should count a TR as a background element for TD', function () {
fixture.innerHTML = '<div style="background-color:#007acc;">' +
'<table style="width:100%">' +
Expand Down Expand Up @@ -315,14 +337,14 @@ describe('color.getBackgroundColor', function () {
});

it('should count an implicit label as a background element', function () {
fixture.innerHTML = '<label id="target" style="background-color: #fff;">My label' +
fixture.innerHTML = '<label id="target" style="background-color: #000;">My label' +
'<input type="text">' +
'</label>';
var target = fixture.querySelector('#target');
var bgNodes = [];
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
var actual = axe.commons.color.getBackgroundColor(target, bgNodes);
var expected = new axe.commons.color.Color(255, 255, 255, 1);
var expected = new axe.commons.color.Color(0, 0, 0, 1);
assert.equal(actual.red, expected.red);
assert.equal(actual.green, expected.green);
assert.equal(actual.blue, expected.blue);
Expand Down Expand Up @@ -512,7 +534,7 @@ describe('color.getBackgroundColor', function () {
});

it('returns elements with negative z-index', function () {
fixture.innerHTML = '<div id="sibling" ' +
fixture.innerHTML = '<div id="sibling" ' +
'style="z-index:-1; position:absolute; width:100%; height:2em; background: #000"></div>' +
'<div id="target">Some text</div>';

Expand All @@ -528,7 +550,7 @@ describe('color.getBackgroundColor', function () {
});

it('returns negative z-index elements when body has a background', function () {
fixture.innerHTML = '<div id="sibling" ' +
fixture.innerHTML = '<div id="sibling" ' +
'style="z-index:-1; position:absolute; width:100%; height:2em; background: #000"></div>' +
'<div id="target">Some text</div>';

Expand Down Expand Up @@ -591,7 +613,6 @@ describe('color.getBackgroundColor', function () {
var expected = new axe.commons.color.Color(0, 255, 0, 1);
document.documentElement.style.background = orig;


assert.closeTo(actual.red, expected.red, 0.5);
assert.closeTo(actual.green, expected.green, 0.5);
assert.closeTo(actual.blue, expected.blue, 0.5);
Expand Down Expand Up @@ -626,4 +647,172 @@ describe('color.getBackgroundColor', function () {
assert.closeTo(actual.alpha, 1, 0.1);
});

it('should return the body bgColor when content does not overlap', function () {
fixture.innerHTML = '<div style="height: 20px; width: 30px; background-color: red;">' +
'<div id="target" style="height:20px; top: 25px; width: 45px; position:absolute;">Text' +
'</div></div>';
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
var target = fixture.querySelector('#target');
var actual = axe.commons.color.getBackgroundColor(target, []);

assert.closeTo(actual.red, 255, 0);
assert.closeTo(actual.green, 255, 0);
assert.closeTo(actual.blue, 255, 0);
assert.closeTo(actual.alpha, 1, 0);
});

(shadowSupported ? it : xit)
('finds colors in shadow boundaries', function () {
fixture.innerHTML = '<div id="container"></div>';
var container = fixture.querySelector('#container');
var shadow = container.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div style="background-color: black;">' +
'<span id="shadowTarget" style="color: #ccc;">Text</span>' +
'</div>';
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);

var target = shadow.querySelector('#shadowTarget');
var actual = axe.commons.color.getBackgroundColor(target, []);

assert.closeTo(actual.red, 0, 0);
assert.closeTo(actual.green, 0, 0);
assert.closeTo(actual.blue, 0, 0);
assert.closeTo(actual.alpha, 1, 0);
});

(shadowSupported ? it : xit)
('finds colors across shadow boundaries', function () {
fixture.innerHTML = '<div id="container" style="background-color:black;"></div>';
var container = fixture.querySelector('#container');
var shadow = container.attachShadow({ mode: 'open' });
shadow.innerHTML = '<span id="shadowTarget" style="color:#ccc;">Text</span>';
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);

var target = shadow.querySelector('#shadowTarget');
var actual = axe.commons.color.getBackgroundColor(target, [], false);

assert.equal(actual.red, 0);
assert.equal(actual.green, 0);
assert.equal(actual.blue, 0);
assert.equal(actual.alpha, 1);
});

(shadowSupported ? it : xit)
('should count an implicit label as a background element inside shadow dom', function () {
fixture.innerHTML = '<div id="container"></div>';
var container = fixture.querySelector('#container');
var shadow = container.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div><label id="target" style="background-color:#000;">Text<input type="text"></label></div>';

var target = shadow.querySelector('#target');
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
var actual = axe.commons.color.getBackgroundColor(target, []);
var expected = new axe.commons.color.Color(0, 0, 0, 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);
});

(shadowSupported ? it : xit)
('finds colors for absolutely positioned elements across shadow boundaries', function () {
fixture.innerHTML = '<div id="container" style="background-color:black; height:20px; position:relative;"></div>';
var container = fixture.querySelector('#container');
var shadow = container.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="shadowTarget" style="color:#333; height:20px; position:absolute; top:20px;">Text</div>';
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);

var target = shadow.querySelector('#shadowTarget');
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
var actual = axe.commons.color.getBackgroundColor(target, []);
assert.equal(actual.red, 255);
assert.equal(actual.green, 255);
assert.equal(actual.blue, 255);
assert.equal(actual.alpha, 1);
});

(shadowSupported ? it : xit)
('finds a color for absolutely positioned content when background is in shadow dom', function () {
fixture.innerHTML = '<div id="elm1" style="width:10em; height:0; position:absolute;"></div>' +
'<div id="elm2" style="color:green; position:absolute;">Text</div>';

var elm1 = document.querySelector('#elm1');
var shadow1 = elm1.attachShadow({ mode: 'open' });
shadow1.innerHTML = '<div style="background:rgba(0,0,0,1); height:10em;"></div>';
var elm2 = document.querySelector('#elm2');
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
var actual = axe.commons.color.getBackgroundColor(elm2, []);
assert.equal(actual.red, 0);
assert.equal(actual.blue, 0);
assert.equal(actual.green, 0);
assert.equal(actual.alpha, 1);
});

(shadowSupported ? it : xit)
('finds colors for content rendered across multiple shadow boundaries', function () {
fixture.innerHTML = '<div style="position:relative;"><div id="elm1" style="width:10em;"></div>' +
'<div id="elm2"></div></div>';

var elm1 = document.querySelector('#elm1');
var shadow1 = elm1.attachShadow({ mode: 'open' });
shadow1.innerHTML = '<div style="background:rgba(0,0,0,1); height:10em;"></div>';
var elm2 = document.querySelector('#elm2');
var shadow2 = elm2.attachShadow({ mode: 'open' });
shadow2.innerHTML = ''+
'<div id="elm3" style="background:rgba(255,255,255,0.5);color:green;height:10em;top:0;position:absolute;">' +
'Text' +
'</div>';

var elm3 = shadow2.querySelector('#elm3');
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
var actual = axe.commons.color.getBackgroundColor(elm3, []);
assert.closeTo(actual.red, 128, 2);
assert.closeTo(actual.blue, 128, 2);
assert.closeTo(actual.green, 128, 2);
assert.closeTo(actual.alpha, 1, 0);
});

(shadowSupported ? it : xit)
('finds colors for multiline elements across shadow boundaries', function () {
fixture.innerHTML = '<div id="container" style="background-color:black; height:40px;"></div>';
var container = fixture.querySelector('#container');
var shadow = container.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="shadowTarget" style="color:#333;">Text<br>More text</div>';
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
var target = shadow.querySelector('#shadowTarget');
var actual = axe.commons.color.getBackgroundColor(target, []);
assert.equal(actual.red, 0);
assert.equal(actual.green, 0);
assert.equal(actual.blue, 0);
assert.equal(actual.alpha, 1);
});

(shadowSupported ? xit : xit)
('returns null for multiline elements not fully covering parents across shadow boundaries', function () {
fixture.innerHTML = '<div id="container" style="background-color:black; height:20px;"></div>';
var container = fixture.querySelector('#container');
var shadow = container.attachShadow({ mode: 'open' });
shadow.innerHTML = '<div id="shadowTarget" style="color:#333;">Text<br>More text</div>';
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
var target = shadow.querySelector('#shadowTarget');
var actual = axe.commons.color.getBackgroundColor(target, []);
assert.isNull(actual);
});

(shadowSupported ? it : xit)
('returns a color for slotted content', function () {
fixture.innerHTML = '<div id="container"></div>';
var div = fixture.querySelector('#container');
div.innerHTML = '<a href="">Link</a>';
var shadow = div.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p style="background-color: #000;"><slot></slot></p>';
axe._tree = axe.utils.getFlattenedTree(fixture.firstChild);
var linkElm = div.querySelector('a');
var actual = axe.commons.color.getBackgroundColor(linkElm, []);
assert.equal(actual.red, 0);
assert.equal(actual.green, 0);
assert.equal(actual.blue, 0);
assert.equal(actual.alpha, 1);
});
});
Loading

0 comments on commit 0440c9b

Please sign in to comment.