diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index e10b0bc6bf..a87816f643 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -10,11 +10,10 @@ import { getForegroundColor, incompleteData, getContrast, - getOwnBackgroundColor, getTextShadowColors, flattenShadowColors } from '../../commons/color'; -import { memoize } from '../../core/utils'; +import { findPseudoElement } from '../../commons/dom'; export default function colorContrastEvaluate(node, options, virtualNode) { const { @@ -72,7 +71,11 @@ export default function colorContrastEvaluate(node, options, virtualNode) { } const bgNodes = []; - const bgColor = getBackgroundColor(node, bgNodes, shadowOutlineEmMax); + const bgColor = getBackgroundColor(node, bgNodes, { + shadowOutlineEmMax, + ignorePseudo, + pseudoSizeThreshold + }); const fgColor = getForegroundColor(node, false, bgColor); // Thin shadows only. Thicker shadows are included in the background instead const shadowColors = getTextShadowColors(node, { @@ -162,57 +165,6 @@ export default function colorContrastEvaluate(node, options, virtualNode) { return isValid; } -function findPseudoElement( - vNode, - { pseudoSizeThreshold = 0.25, ignorePseudo = false } -) { - if (ignorePseudo) { - return; - } - const rect = vNode.boundingClientRect; - const minimumSize = rect.width * rect.height * pseudoSizeThreshold; - do { - const beforeSize = getPseudoElementArea(vNode.actualNode, ':before'); - const afterSize = getPseudoElementArea(vNode.actualNode, ':after'); - if (beforeSize + afterSize > minimumSize) { - return vNode; // Combined area of before and after exceeds the minimum size - } - } while ((vNode = vNode.parent)); -} - -const getPseudoElementArea = memoize(function getPseudoElementArea( - node, - pseudo -) { - const style = window.getComputedStyle(node, pseudo); - const matchPseudoStyle = (prop, value) => - style.getPropertyValue(prop) === value; - if ( - matchPseudoStyle('content', 'none') || - matchPseudoStyle('display', 'none') || - matchPseudoStyle('visibility', 'hidden') || - matchPseudoStyle('position', 'absolute') === false - ) { - return 0; // The pseudo element isn't visible - } - - if ( - getOwnBackgroundColor(style).alpha === 0 && - matchPseudoStyle('background-image', 'none') - ) { - return 0; // There is no background - } - - // Find the size of the pseudo element; - const pseudoWidth = parseUnit(style.getPropertyValue('width')); - const pseudoHeight = parseUnit(style.getPropertyValue('height')); - if (pseudoWidth.unit !== 'px' || pseudoHeight.unit !== 'px') { - // IE doesn't normalize to px. Infinity gets everything to undefined - return pseudoWidth.value === 0 || pseudoHeight.value === 0 ? 0 : Infinity; - } - return pseudoWidth.value * pseudoHeight.value; -}); - function textIsEmojis(visibleText) { const options = { nonBmp: true }; const hasUnicodeChars = hasUnicode(visibleText, options); @@ -220,12 +172,3 @@ function textIsEmojis(visibleText) { sanitize(removeUnicode(visibleText, options)) === ''; return hasUnicodeChars && hasNonUnicodeChars; } - -function parseUnit(str) { - const unitRegex = /^([0-9.]+)([a-z]+)$/i; - const [, value = '', unit = ''] = str.match(unitRegex) || []; - return { - value: parseFloat(value), - unit: unit.toLowerCase() - }; -} diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js index 499511b770..3976c9bd32 100644 --- a/lib/commons/color/get-background-color.js +++ b/lib/commons/color/get-background-color.js @@ -18,19 +18,27 @@ import visuallyContains from '../dom/visually-contains'; * @param {Element} elm Element to determine background color * @param {Array} [bgElms=[]] elements to inspect * @param {Number} shadowOutlineEmMax Thickness of `text-shadow` at which it becomes a background color + * @param {Object} options + * @deprecated shadowOutlineEmMax parameter. Pass an object instead with shadowOutlineEmMax as property. * @returns {Color} */ export default function getBackgroundColor( elm, bgElms = [], - shadowOutlineEmMax = 0.1 + shadowOutlineEmMax, + options ) { + if (['undefined', 'object'].includes(typeof shadowOutlineEmMax)) { + options = shadowOutlineEmMax ?? {}; + shadowOutlineEmMax = options.shadowOutlineEmMax ?? 0.1; + } + let bgColors = getTextShadowColors(elm, { minRatio: shadowOutlineEmMax }); if (bgColors.length) { bgColors = [{ color: bgColors.reduce(flattenShadowColors) }]; } - const elmStack = getBackgroundStack(elm); + const elmStack = getBackgroundStack(elm, options); // Search the stack until we have an alpha === 1 background (elmStack || []).some(bgElm => { diff --git a/lib/commons/color/get-background-stack.js b/lib/commons/color/get-background-stack.js index 10a54dccfe..72f0a76e2e 100644 --- a/lib/commons/color/get-background-stack.js +++ b/lib/commons/color/get-background-stack.js @@ -3,50 +3,37 @@ import elementHasImage from './element-has-image'; import getOwnBackgroundColor from './get-own-background-color'; import incompleteData from './incomplete-data'; import reduceToElementsBelowFloating from '../dom/reduce-to-elements-below-floating'; +import visibleTextNodes from '../text/visible-text-nodes'; +import findPseudoElement from '../dom/find-pseudo-element'; +import { getNodeFromTree } from '../../core/utils'; /** - * Determine if element B is an inline descendant of A - * @private - * @param {Element} node - * @param {Element} descendant - * @return {Boolean} + * Get all elements rendered underneath the current element, + * In the order they are displayed (front to back) + * + * @method getBackgroundStack + * @memberof axe.commons.color + * @param {Element} elm + * @param {Object} options + * @return {Array} */ -function isInlineDescendant(node, descendant) { - const CONTAINED_BY = Node.DOCUMENT_POSITION_CONTAINED_BY; - // eslint-disable-next-line no-bitwise - if (!(node.compareDocumentPosition(descendant) & CONTAINED_BY)) { - return false; - } - const style = window.getComputedStyle(descendant); - const display = style.getPropertyValue('display'); - if (!display.includes('inline')) { - return false; +export default function getBackgroundStack(elm, options) { + let elmStack = filteredRectStack(elm); + + if (elmStack === null) { + return null; } - // IE needs this; It doesn't set display:block when position is set - const position = style.getPropertyValue('position') - return position === 'static'; -} + elmStack = reduceToElementsBelowFloating(elmStack, elm); + elmStack = sortPageBackground(elmStack); -/** - * Determine if the element obscures / overlaps with the text - * @private - * @param {Number} elmIndex - * @param {Array} elmStack - * @param {Element} originalElm - * @return {Number|undefined} - */ -function calculateObscuringElement(elmIndex, elmStack, originalElm) { - // Reverse order, so that we can safely splice - for (let i = elmIndex - 1; i >= 0; i--) { - if (!isInlineDescendant(originalElm, elmStack[i])) { - return true; - } - // Ignore inline descendants, for example: - //

text

; We don't care about the element, - // since it does not overlap the text inside of

- elmStack.splice(i, 1); + // Return all elements BELOW the current element, null if the element is undefined + const elmIndex = elmStack.indexOf(elm); + if (calculateObscuringElement(elmIndex, elmStack, elm, options)) { + // if the total of the elements above our element results in total obscuring, return null + incompleteData.set('bgColor', 'bgOverlap'); + return null; } - return false; + return elmIndex !== -1 ? elmStack : null; } /** @@ -95,31 +82,89 @@ function sortPageBackground(elmStack) { } /** - * Get all elements rendered underneath the current element, - * In the order they are displayed (front to back) - * - * @method getBackgroundStack - * @memberof axe.commons.color - * @param {Element} elm - * @return {Array} + * Determine if the element obscures / overlaps with the text + * @private + * @param {Number} elmIndex + * @param {Array} elmStack + * @param {Element} originalElm + * @param {Object} options + * @return {Number|undefined} */ -function getBackgroundStack(elm) { - let elmStack = filteredRectStack(elm); - - if (elmStack === null) { - return null; +function calculateObscuringElement(elmIndex, elmStack, originalElm, options) { + // Reverse order, so that we can safely splice + for (let i = elmIndex - 1; i >= 0; i--) { + if ( + !isInlineDescendant(originalElm, elmStack[i]) && + !isEmptyElement(elmStack[i], options) + ) { + return true; + } + // Ignore inline descendants, for example: + //

text

; We don't care about the element, + // since it does not overlap the text inside of

+ elmStack.splice(i, 1); } - elmStack = reduceToElementsBelowFloating(elmStack, elm); - elmStack = sortPageBackground(elmStack); + return false; +} - // Return all elements BELOW the current element, null if the element is undefined - const elmIndex = elmStack.indexOf(elm); - if (calculateObscuringElement(elmIndex, elmStack, elm)) { - // if the total of the elements above our element results in total obscuring, return null - incompleteData.set('bgColor', 'bgOverlap'); - return null; +/** + * Determine if element B is an inline descendant of A + * @private + * @param {Element} node + * @param {Element} descendant + * @return {Boolean} + */ +function isInlineDescendant(node, descendant) { + const CONTAINED_BY = Node.DOCUMENT_POSITION_CONTAINED_BY; + // eslint-disable-next-line no-bitwise + if (!(node.compareDocumentPosition(descendant) & CONTAINED_BY)) { + return false; } - return elmIndex !== -1 ? elmStack : null; + const style = window.getComputedStyle(descendant); + const display = style.getPropertyValue('display'); + if (!display.includes('inline')) { + return false; + } + // IE needs this; It doesn't set display:block when position is set + const position = style.getPropertyValue('position'); + return position === 'static'; } -export default getBackgroundStack; +/** + * Determine if an element is empty and would not contribute to the background color + * @see https://github.com/dequelabs/axe-core/issues/3464 + * @private + * @param {Element} node + * @param {Object} options + */ +function isEmptyElement(node, { pseudoSizeThreshold, ignorePseudo }) { + const vNode = getNodeFromTree(node); + const style = window.getComputedStyle(node); + const bgColor = getOwnBackgroundColor(style); + const hasPseudoElement = !!findPseudoElement(vNode, { + pseudoSizeThreshold, + ignorePseudo, + recurse: false + }); + // IE11 does not support border, border-width, or + // outline computed shorthand properties + const hasBorder = [ + 'border-bottom-width', + 'border-top-width', + 'border-left-width', + 'border-right-width', + 'outline-width' + ].some(prop => parseInt(vNode.getComputedStylePropertyValue(prop), 10) !== 0); + + if ( + visibleTextNodes(vNode).length === 0 && + bgColor.alpha === 0 && + !elementHasImage(node, style) && + !hasPseudoElement && + !hasBorder + ) { + return true; + } + + return false; +} diff --git a/lib/commons/dom/find-pseudo-element.js b/lib/commons/dom/find-pseudo-element.js new file mode 100644 index 0000000000..5c4689d9c5 --- /dev/null +++ b/lib/commons/dom/find-pseudo-element.js @@ -0,0 +1,71 @@ +import memoize from '../../core/utils/memoize'; +import getOwnBackgroundColor from '../color/get-own-background-color'; + +/** + * Find the pseudo element of node that meets the minimum size threshold. + * @method findPseudoElement + * @memberof axe.commons.dom + * @instance + * @param {VirtualNode} vNode The VirtualNode to find pseudo elements for + * @param {Object} options + * @returns {VirtualNode|undefined} The VirtualNode which has matching pseudo elements. + */ +export default function findPseudoElement( + vNode, + { pseudoSizeThreshold = 0.25, ignorePseudo = false, recurse = true } = {} +) { + if (ignorePseudo) { + return; + } + const rect = vNode.boundingClientRect; + const minimumSize = rect.width * rect.height * pseudoSizeThreshold; + do { + const beforeSize = getPseudoElementArea(vNode.actualNode, ':before'); + const afterSize = getPseudoElementArea(vNode.actualNode, ':after'); + if (beforeSize + afterSize > minimumSize) { + return vNode; // Combined area of before and after exceeds the minimum size + } + } while (recurse && (vNode = vNode.parent)); +} + +const getPseudoElementArea = memoize(function getPseudoElementArea( + node, + pseudo +) { + const style = window.getComputedStyle(node, pseudo); + const matchPseudoStyle = (prop, value) => + style.getPropertyValue(prop) === value; + if ( + matchPseudoStyle('content', 'none') || + matchPseudoStyle('display', 'none') || + matchPseudoStyle('visibility', 'hidden') || + matchPseudoStyle('position', 'absolute') === false + ) { + return 0; // The pseudo element isn't visible + } + + if ( + getOwnBackgroundColor(style).alpha === 0 && + matchPseudoStyle('background-image', 'none') + ) { + return 0; // There is no background + } + + // Find the size of the pseudo element; + const pseudoWidth = parseUnit(style.getPropertyValue('width')); + const pseudoHeight = parseUnit(style.getPropertyValue('height')); + if (pseudoWidth.unit !== 'px' || pseudoHeight.unit !== 'px') { + // IE doesn't normalize to px. Infinity gets everything to undefined + return pseudoWidth.value === 0 || pseudoHeight.value === 0 ? 0 : Infinity; + } + return pseudoWidth.value * pseudoHeight.value; +}); + +function parseUnit(str) { + const unitRegex = /^([0-9.]+)([a-z]+)$/i; + const [, value = '', unit = ''] = str.match(unitRegex) || []; + return { + value: parseFloat(value), + unit: unit.toLowerCase() + }; +} diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 6e60cec766..9572f03ed2 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -4,6 +4,7 @@ * @memberof axe.commons */ export { default as findElmsInContext } from './find-elms-in-context'; +export { default as findPseudoElement } from './find-pseudo-element'; export { default as findUpVirtual } from './find-up-virtual'; export { default as findUp } from './find-up'; export { default as getComposedParent } from './get-composed-parent'; diff --git a/test/commons/color/get-background-color.js b/test/commons/color/get-background-color.js index 9fc20d7a3e..672e531441 100644 --- a/test/commons/color/get-background-color.js +++ b/test/commons/color/get-background-color.js @@ -1,4 +1,4 @@ -describe('color.getBackgroundColor', function() { +describe('color.getBackgroundColor', function () { 'use strict'; var fixture = document.getElementById('fixture'); @@ -8,12 +8,12 @@ describe('color.getBackgroundColor', function() { var origBodyBg; var origHtmlBg; - before(function() { + before(function () { origBodyBg = document.body.style.background; origHtmlBg = document.documentElement.style.background; }); - afterEach(function() { + afterEach(function () { document.body.style.background = origBodyBg; document.documentElement.style.background = origHtmlBg; @@ -21,7 +21,7 @@ describe('color.getBackgroundColor', function() { axe._tree = undefined; }); - it('should return the blended color if it has no background set', function() { + it('should return the blended color if it has no background set', function () { fixture.innerHTML = '

' + '
' + @@ -39,7 +39,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('should return the blended color if it is transparent and positioned', function() { + it('should return the blended color if it is transparent and positioned', function () { fixture.innerHTML = '
' + @@ -64,7 +64,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [target, pos]); }); - it('should do alpha blending from the back forward', function() { + it('should do alpha blending from the back forward', function () { fixture.innerHTML = '
' + '
' + @@ -83,7 +83,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [target, under]); }); - it('should only look at what is underneath original element when blended and positioned', function() { + it('should only look at what is underneath original element when blended and positioned', function () { fixture.innerHTML = '
' + @@ -110,7 +110,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [target, under]); }); - it('should return the proper blended color if it has alpha set', function() { + it('should return the proper blended color if it has alpha set', function () { fixture.innerHTML = '
' + '
' + @@ -128,7 +128,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [target, parent]); }); - it('should return the blended color if it has opacity set', function() { + it('should return the blended color if it has opacity set', function () { fixture.innerHTML = '
' + '
' + @@ -146,7 +146,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [target, parent]); }); - it('should return null if containing parent has a background image and is non-opaque', function() { + it('should return null if containing parent has a background image and is non-opaque', function () { fixture.innerHTML = '
' + @@ -162,7 +162,7 @@ describe('color.getBackgroundColor', function() { assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'bgImage'); }); - it('should return white if transparency goes all the way up to document', function() { + it('should return white if transparency goes all the way up to document', function () { fixture.innerHTML = '
'; var target = fixture.querySelector('#target'); axe.testUtils.flatTreeSetup(fixture); @@ -174,7 +174,7 @@ describe('color.getBackgroundColor', function() { assert.equal(actual.alpha, expected.alpha); }); - it('should return null if there is a background image', function() { + it('should return null if there is a background image', function () { fixture.innerHTML = '
' + '
' + @@ -188,7 +188,7 @@ describe('color.getBackgroundColor', function() { assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'bgImage'); }); - it('should return null if something non-opaque is obscuring it', function() { + it('should return null if something non-opaque is obscuring it', function () { fixture.innerHTML = '
' + '
Hello
'; @@ -201,7 +201,7 @@ describe('color.getBackgroundColor', function() { assert.isNull(actual); }); - it('should return null if something non-opaque is obscuring it, scrolled out of view', function() { + it('should return null if something non-opaque is obscuring it, scrolled out of view', function () { fixture.innerHTML = '
' + '
' + '
Hello
'; @@ -229,7 +229,7 @@ describe('color.getBackgroundColor', function() { assert.isNull(actual); }); - it('should return the bgcolor if it is solid', function() { + it('should return the bgcolor if it is solid', function () { fixture.innerHTML = '
' + '
' + @@ -246,7 +246,195 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [target]); }); - it('should return a bgcolor for a multiline inline element fully covering the background', function() { + describe('overlapping element', function () { + it('should return bgcolor if an empty element is obscuring it', function () { + fixture.innerHTML = + '' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.isNotNull(actual); + }); + + it('should null if element has text', function () { + fixture.innerHTML = + '
Hell world
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.equal( + axe.commons.color.incompleteData.get('bgColor'), + 'bgOverlap' + ); + assert.isNull(actual); + }); + + it('should null if element has a background color', function () { + fixture.innerHTML = + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.equal( + axe.commons.color.incompleteData.get('bgColor'), + 'bgOverlap' + ); + assert.isNull(actual); + }); + + it('should null if element has a background image', function () { + fixture.innerHTML = + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.equal( + axe.commons.color.incompleteData.get('bgColor'), + 'bgOverlap' + ); + assert.isNull(actual); + }); + + it('should null if element is an image element', function () { + fixture.innerHTML = + '' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.equal( + axe.commons.color.incompleteData.get('bgColor'), + 'bgOverlap' + ); + assert.isNull(actual); + }); + + it('should null if element is an embedded content element', function () { + fixture.innerHTML = + '' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.equal( + axe.commons.color.incompleteData.get('bgColor'), + 'bgOverlap' + ); + assert.isNull(actual); + }); + + it('should null if element has a border', function () { + fixture.innerHTML = + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.equal( + axe.commons.color.incompleteData.get('bgColor'), + 'bgOverlap' + ); + assert.isNull(actual); + }); + + it('should null if element has a single border', function () { + fixture.innerHTML = + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.equal( + axe.commons.color.incompleteData.get('bgColor'), + 'bgOverlap' + ); + assert.isNull(actual); + }); + + it('should null if element has an outline', function () { + fixture.innerHTML = + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.equal( + axe.commons.color.incompleteData.get('bgColor'), + 'bgOverlap' + ); + assert.isNull(actual); + }); + + it('should null if element has a pseudo element with a background', function () { + fixture.innerHTML = + '' + + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.equal( + axe.commons.color.incompleteData.get('bgColor'), + 'bgOverlap' + ); + assert.isNull(actual); + }); + + it('should return bgcolor if element has a pseudo element without a background', function () { + fixture.innerHTML = + '' + + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.isNotNull(actual); + }); + + it('should return bgcolor if element is non-opaque', function () { + fixture.innerHTML = + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, []); + assert.isNotNull(actual); + }); + + describe('options', function () { + it('should ignore pseudo elements with "ignorePseudo"', function () { + fixture.innerHTML = + '' + + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, [], { + ignorePseudo: true + }); + assert.isNotNull(actual); + }); + + it('should find pseudo elements with "pseudoSizeThreshold"', function () { + fixture.innerHTML = + '' + + '
' + + '
Hello
'; + var target = fixture.querySelector('#target'); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(target, [], { + pseudoSizeThreshold: 0.05 + }); + assert.isNotNull(actual); + }); + }); + }); + + it('should return a bgcolor for a multiline inline element fully covering the background', function () { fixture.innerHTML = '
' + '
' + @@ -263,7 +451,7 @@ describe('color.getBackgroundColor', function() { assert.equal(Math.round(actual.green), 0); }); - it('should return null if a multiline inline element does not fully cover background', function() { + it('should return null if a multiline inline element does not fully cover background', function () { fixture.innerHTML = '
' + '
' + @@ -281,7 +469,7 @@ describe('color.getBackgroundColor', function() { ); }); - it('should return an actual if an absolutely positioned element does not cover background', function() { + it('should return an actual if an absolutely positioned element does not cover background', function () { fixture.innerHTML = '
' + '
Text
' + @@ -296,7 +484,7 @@ describe('color.getBackgroundColor', function() { assert.equal(Math.round(actual.green), 255); }); - it('should return null if an absolutely positioned element partially obsures background', function() { + it('should return null if an absolutely positioned element partially obsures background', function () { fixture.innerHTML = '
' + '
' + @@ -314,7 +502,7 @@ describe('color.getBackgroundColor', function() { ); }); - it('should count a TR as a background element for TD', function() { + it('should count a TR as a background element for TD', function () { fixture.innerHTML = '
' + '' + @@ -336,7 +524,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('should count a TR as a background element for TH', function() { + it('should count a TR as a background element for TH', function () { fixture.innerHTML = '
' + '
' + @@ -358,7 +546,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('should count a TR as a background element for a child element', function() { + it('should count a TR as a background element for a child element', function () { fixture.innerHTML = '
' + '
' + @@ -380,7 +568,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('should count a THEAD as a background element for a child element', function() { + it('should count a THEAD as a background element for a child element', function () { fixture.innerHTML = '
' + '
' + @@ -402,7 +590,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('should count a TBODY as a background element for a child element', function() { + it('should count a TBODY as a background element for a child element', function () { fixture.innerHTML = '
' + '
' + @@ -424,7 +612,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('should count a TFOOT as a background element for a child element', function() { + it('should count a TFOOT as a background element for a child element', function () { fixture.innerHTML = '
' + '
' + @@ -446,7 +634,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it("should ignore TR elements that don't overlap", function() { + it("should ignore TR elements that don't overlap", function () { fixture.innerHTML = '
' + '' + @@ -465,7 +653,7 @@ describe('color.getBackgroundColor', function() { assert.notEqual(bgNodes, [parent]); }); - it('should count an implicit label as a background element', function() { + it('should count an implicit label as a background element', function () { fixture.innerHTML = '
' + @@ -560,7 +748,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('should use hierarchical DOM traversal if possible', function() { + it('should use hierarchical DOM traversal if possible', function () { fixture.innerHTML = '
' + @@ -583,7 +771,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('should ignore 0-height elements', function() { + it('should ignore 0-height elements', function () { fixture.innerHTML = '
' + @@ -604,7 +792,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('should use visual traversal when needed', function() { + it('should use visual traversal when needed', function () { fixture.innerHTML = '
' + @@ -628,7 +816,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('should return null when encountering background images during visual traversal', function() { + it('should return null when encountering background images during visual traversal', function () { fixture.innerHTML = '
' + @@ -648,7 +836,7 @@ describe('color.getBackgroundColor', function() { assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'bgImage'); }); - it('should return null when encountering image nodes during visual traversal', function() { + it('should return null when encountering image nodes during visual traversal', function () { fixture.innerHTML = '
' + @@ -668,7 +856,7 @@ describe('color.getBackgroundColor', function() { assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'imgNode'); }); - it('returns elements with negative z-index', function() { + it('returns elements with negative z-index', function () { fixture.innerHTML = '
' + @@ -688,7 +876,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('returns negative z-index elements when body has a background', function() { + it('returns negative z-index elements when body has a background', function () { fixture.innerHTML = '
' + @@ -709,7 +897,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('should return null for negative z-index element when html and body have a background', function() { + it('should return null for negative z-index element when html and body have a background', function () { fixture.innerHTML = '
' + '
Text' + @@ -761,7 +949,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, 1, 0); }); - it('should return the html canvas inherited from body bgColor when element content does not overlap with body', function() { + it('should return the html canvas inherited from body bgColor when element content does not overlap with body', function () { fixture.innerHTML = '
Text
'; @@ -785,7 +973,7 @@ describe('color.getBackgroundColor', function() { document.body.style.margin = originalMargin; }); - it('should return the html canvas bgColor when element content does not overlap with body', function() { + it('should return the html canvas bgColor when element content does not overlap with body', function () { fixture.innerHTML = '
Text
'; @@ -807,28 +995,31 @@ describe('color.getBackgroundColor', function() { document.body.style.height = originalHeight; }); - (shadowSupported ? it : xit)('finds colors in shadow boundaries', function() { - fixture.innerHTML = '
'; - var container = fixture.querySelector('#container'); - var shadow = container.attachShadow({ mode: 'open' }); - shadow.innerHTML = - '
' + - 'Text' + - '
'; - axe.testUtils.flatTreeSetup(fixture); + (shadowSupported ? it : xit)( + 'finds colors in shadow boundaries', + function () { + fixture.innerHTML = '
'; + var container = fixture.querySelector('#container'); + var shadow = container.attachShadow({ mode: 'open' }); + shadow.innerHTML = + '
' + + 'Text' + + '
'; + axe.testUtils.flatTreeSetup(fixture); - var target = shadow.querySelector('#shadowTarget'); - var actual = axe.commons.color.getBackgroundColor(target, []); + 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); - }); + 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() { + function () { fixture.innerHTML = '
'; var container = fixture.querySelector('#container'); @@ -849,7 +1040,7 @@ describe('color.getBackgroundColor', function() { (shadowSupported ? it : xit)( 'should count an implicit label as a background element inside shadow dom', - function() { + function () { fixture.innerHTML = '
'; var container = fixture.querySelector('#container'); var shadow = container.attachShadow({ mode: 'open' }); @@ -870,7 +1061,7 @@ describe('color.getBackgroundColor', function() { (shadowSupported ? it : xit)( 'finds colors for absolutely positioned elements across shadow boundaries', - function() { + function () { fixture.innerHTML = '
'; var container = fixture.querySelector('#container'); @@ -891,7 +1082,7 @@ describe('color.getBackgroundColor', function() { (shadowSupported ? it : xit)( 'finds a color for absolutely positioned content when background is in shadow dom', - function() { + function () { fixture.innerHTML = '
' + '
Text
'; @@ -912,7 +1103,7 @@ describe('color.getBackgroundColor', function() { (shadowSupported ? it : xit)( 'finds colors for content rendered across multiple shadow boundaries', - function() { + function () { fixture.innerHTML = '
' + '
'; @@ -941,7 +1132,7 @@ describe('color.getBackgroundColor', function() { (shadowSupported ? it : xit)( 'finds colors for multiline elements across shadow boundaries', - function() { + function () { fixture.innerHTML = '
'; var container = fixture.querySelector('#container'); @@ -960,7 +1151,7 @@ describe('color.getBackgroundColor', function() { (shadowSupported ? xit : xit)( 'returns null for multiline elements not fully covering parents across shadow boundaries', - function() { + function () { fixture.innerHTML = '
'; var container = fixture.querySelector('#container'); @@ -976,7 +1167,7 @@ describe('color.getBackgroundColor', function() { (shadowSupported ? it : xit)( 'returns a color for slotted content', - function() { + function () { fixture.innerHTML = '
'; var div = fixture.querySelector('#container'); div.innerHTML = 'Link'; @@ -992,7 +1183,7 @@ describe('color.getBackgroundColor', function() { } ); - it('should return the text-shadow mixed in with the background', function() { + it('should return the text-shadow mixed in with the background', function () { fixture.innerHTML = '
' + '
foo' + @@ -1012,7 +1203,7 @@ describe('color.getBackgroundColor', function() { assert.deepEqual(bgNodes, [parent]); }); - it('ignores thin text-shadows', function() { + it('ignores thin text-shadows', function () { fixture.innerHTML = '
' + '
foo' + @@ -1028,7 +1219,7 @@ describe('color.getBackgroundColor', function() { assert.equal(actual.alpha, 1); }); - it('ignores text-shadows thinner than shadowOutlineEmMax', function() { + it('ignores text-shadows thinner than shadowOutlineEmMax', function () { fixture.innerHTML = '
' + '
foo' + @@ -1046,8 +1237,8 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - describe('body and document', function() { - it('returns the body background', function() { + describe('body and document', function () { + it('returns the body background', function () { fixture.innerHTML = '
elm
'; document.body.style.background = '#F00'; @@ -1064,7 +1255,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('returns the body background even when the body is MUCH larger than the screen', function() { + it('returns the body background even when the body is MUCH larger than the screen', function () { fixture.innerHTML = '
elm
'; document.body.style.background = '#F00'; @@ -1081,7 +1272,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('returns the html background', function() { + it('returns the html background', function () { fixture.innerHTML = '
'; document.documentElement.style.background = '#0F0'; @@ -1098,7 +1289,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('returns the html background when body does not cover the element', function() { + it('returns the html background when body does not cover the element', function () { fixture.innerHTML = '
elm
'; document.documentElement.style.background = '#0F0'; @@ -1117,7 +1308,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('returns the body background when body does cover the element', function() { + it('returns the body background when body does cover the element', function () { fixture.innerHTML = '
'; document.documentElement.style.background = '#0F0'; document.body.style.background = '#00F'; @@ -1135,7 +1326,7 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('returns both the html and body background if the body has alpha', function() { + it('returns both the html and body background if the body has alpha', function () { fixture.innerHTML = '
'; document.documentElement.style.background = '#0F0'; document.body.style.background = 'rgba(0, 0, 255, 0.5)'; diff --git a/test/commons/dom/find-pseudo-element.js b/test/commons/dom/find-pseudo-element.js new file mode 100644 index 0000000000..12f97c1f63 --- /dev/null +++ b/test/commons/dom/find-pseudo-element.js @@ -0,0 +1,169 @@ +describe('dom.findPseudoElement', function () { + var queryFixture = axe.testUtils.queryFixture; + var findPseudoElement = axe.commons.dom.findPseudoElement; + + var pseudoElms = ['::before', '::after']; + for (var i = 0; i < pseudoElms.length; i++) { + var pseudoElm = pseudoElms[i]; + describe(pseudoElm, function () { + it('should return the element with a pseudo element (background color)', function () { + var vNode = queryFixture( + '' + + '
' + ); + var actual = findPseudoElement(vNode); + assert.equal(vNode, actual); + }); + + it('should return the element with a pseudo element (background image)', function () { + var vNode = queryFixture( + '' + + '
' + ); + var actual = findPseudoElement(vNode); + assert.equal(vNode, actual); + }); + + it('should return a parent element with a pseudo element', function () { + var vNode = queryFixture( + '
' + ); + var target = axe.utils.querySelectorAll(axe._tree, '.parent')[0]; + var actual = findPseudoElement(vNode); + assert.equal(target, actual); + }); + + it('should return undefined if pseudo element has no content', function () { + var vNode = queryFixture( + '
' + ); + var actual = findPseudoElement(vNode); + assert.isUndefined(actual); + }); + + it('should return undefined if pseudo element is hidden (display: none)', function () { + var vNode = queryFixture( + '
' + ); + var actual = findPseudoElement(vNode); + assert.isUndefined(actual); + }); + + it('should return undefined if pseudo element is hidden (visibility: hidden)', function () { + var vNode = queryFixture( + '
' + ); + var actual = findPseudoElement(vNode); + assert.isUndefined(actual); + }); + + it('should return undefined if pseudo element is hidden (position not absolute)', function () { + var vNode = queryFixture( + '
' + ); + var actual = findPseudoElement(vNode); + assert.isUndefined(actual); + }); + + it('should return undefined if pseudo element does not have a background', function () { + var vNode = queryFixture( + '
' + ); + var actual = findPseudoElement(vNode); + assert.isUndefined(actual); + }); + + it('should return undefined if pseudo element has a transparent background', function () { + var vNode = queryFixture( + '
' + ); + var actual = findPseudoElement(vNode); + assert.isUndefined(actual); + }); + + it('should return undefined if pseudo element is too small', function () { + var vNode = queryFixture( + '
' + ); + var actual = findPseudoElement(vNode); + assert.isUndefined(actual); + }); + + it('should return undefined if pseudo size is equal to "pseudoSizeThreshold"', function () { + var vNode = queryFixture( + '' + + '
' + ); + var actual = findPseudoElement(vNode); + assert.isUndefined(actual); + }); + + it('should return the element if pseudo size is greater than "pseudoSizeThreshold"', function () { + var vNode = queryFixture( + '' + + '
' + ); + var actual = findPseudoElement(vNode); + assert.equal(vNode, actual); + }); + + describe('options', function () { + it('should return undefined if passed "ignorePseudo: true"', function () { + var vNode = queryFixture( + '' + + '
' + ); + var actual = findPseudoElement(vNode, { ignorePseudo: true }); + assert.isUndefined(actual); + }); + + it('should return the element for "pseudoSizeThreshold"', function () { + var vNode = queryFixture( + '' + + '
' + ); + var actual = findPseudoElement(vNode, { pseudoSizeThreshold: 0.05 }); + assert.equal(vNode, actual); + }); + + it('should return undefined for parent with pseudo if passed "recurse: false"', function () { + var vNode = queryFixture( + '
' + ); + var actual = findPseudoElement(vNode, { recurse: false }); + assert.isUndefined(actual); + }); + }); + }); + } +}); diff --git a/test/integration/rules/color-contrast/color-contrast.html b/test/integration/rules/color-contrast/color-contrast.html index a1e2ed0997..c65cb6a836 100644 --- a/test/integration/rules/color-contrast/color-contrast.html +++ b/test/integration/rules/color-contrast/color-contrast.html @@ -1,17 +1,22 @@
This is a pass.
-
+
This is a pass.
This is a pass.
-
+
-
+
This is a fail.But this is a pass.
-
+
Pass.
-
-
-
+
This is a fail.
-
+
This is a fail.
@@ -65,7 +67,7 @@
Hello world
@@ -99,7 +101,13 @@
text
@@ -122,44 +130,63 @@
Background image
-
+
Outside parent
-
-
+
+
Background overlap
Element overlap
-
+
-
-
Hi
+
+
Hi
Hi
-
-
+
+
₠ ₡ ₢ ₣
@@ -256,7 +283,13 @@

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed et sollicitudin @@ -273,3 +306,44 @@ felis ante non libero.

+ +
+ + Hello +
+ + + + diff --git a/test/integration/rules/color-contrast/color-contrast.json b/test/integration/rules/color-contrast/color-contrast.json index c661e1881c..33dc006728 100644 --- a/test/integration/rules/color-contrast/color-contrast.json +++ b/test/integration/rules/color-contrast/color-contrast.json @@ -12,7 +12,9 @@ ["#pass7 > input"], ["#pass8"], ["#text-shadow-fg-pass"], - ["#pass9"] + ["#pass9"], + ["#pass10"], + ["#pass11"] ], "incomplete": [ ["#canttell1"],