diff --git a/lib/checks/color/color-contrast.js b/lib/checks/color/color-contrast.js index c2144a2b9a..e68a1bbe26 100644 --- a/lib/checks/color/color-contrast.js +++ b/lib/checks/color/color-contrast.js @@ -1,42 +1,44 @@ -if (!axe.commons.dom.isVisible(node, false)) { +const { dom, color, text } = axe.commons; + +if (!dom.isVisible(node, false)) { return true; } -var noScroll = !!(options || {}).noScroll; -var bgNodes = [], - bgColor = axe.commons.color.getBackgroundColor(node, bgNodes, noScroll), - fgColor = axe.commons.color.getForegroundColor(node, noScroll); +const noScroll = !!(options || {}).noScroll; +const bgNodes = []; +const bgColor = color.getBackgroundColor(node, bgNodes, noScroll); +const fgColor = color.getForegroundColor(node, noScroll); -var nodeStyle = window.getComputedStyle(node); -var fontSize = parseFloat(nodeStyle.getPropertyValue('font-size')); -var fontWeight = nodeStyle.getPropertyValue('font-weight'); -var bold = +const nodeStyle = window.getComputedStyle(node); +const fontSize = parseFloat(nodeStyle.getPropertyValue('font-size')); +const fontWeight = nodeStyle.getPropertyValue('font-weight'); +const bold = ['bold', 'bolder', '600', '700', '800', '900'].indexOf(fontWeight) !== -1; -var cr = axe.commons.color.hasValidContrastRatio( - bgColor, - fgColor, - fontSize, - bold -); +const cr = color.hasValidContrastRatio(bgColor, fgColor, fontSize, bold); // truncate ratio to three digits while rounding down // 4.499 = 4.49, 4.019 = 4.01 -var truncatedResult = Math.floor(cr.contrastRatio * 100) / 100; +const truncatedResult = Math.floor(cr.contrastRatio * 100) / 100; // if fgColor or bgColor are missing, get more information. -var missing; +let missing; if (bgColor === null) { - missing = axe.commons.color.incompleteData.get('bgColor'); + missing = color.incompleteData.get('bgColor'); } -let equalRatio = false; -if (truncatedResult === 1) { - equalRatio = true; - missing = axe.commons.color.incompleteData.set('bgColor', 'equalRatio'); +const equalRatio = truncatedResult === 1; +const shortTextContent = + text.visibleVirtual(virtualNode, false, true).length === 1; +if (equalRatio) { + missing = color.incompleteData.set('bgColor', 'equalRatio'); +} else if (shortTextContent) { + // Check that the text content is a single character long + missing = 'shortTextContent'; } + // need both independently in case both are missing -var data = { +const data = { fgColor: fgColor ? fgColor.toHexString() : undefined, bgColor: bgColor ? bgColor.toHexString() : undefined, contrastRatio: cr ? truncatedResult : undefined, @@ -48,13 +50,21 @@ var data = { this.data(data); -//We don't know, so we'll put it into Can't Tell -if (fgColor === null || bgColor === null || equalRatio) { +// We don't know, so we'll put it into Can't Tell +if ( + fgColor === null || + bgColor === null || + equalRatio || + (shortTextContent && !cr.isValid) +) { missing = null; - axe.commons.color.incompleteData.clear(); + color.incompleteData.clear(); this.relatedNodes(bgNodes); return undefined; -} else if (!cr.isValid) { +} + +if (!cr.isValid) { this.relatedNodes(bgNodes); } + return cr.isValid; diff --git a/lib/checks/color/color-contrast.json b/lib/checks/color/color-contrast.json index 7787be990f..234bee2593 100644 --- a/lib/checks/color/color-contrast.json +++ b/lib/checks/color/color-contrast.json @@ -16,6 +16,7 @@ "elmPartiallyObscuring": "Element's background color could not be determined because it partially overlaps other elements", "outsideViewport": "Element's background color could not be determined because it's outside the viewport", "equalRatio": "Element has a 1:1 contrast ratio with the background", + "shortTextContent": "Element content is too short to determine if it is actual text content", "default": "Unable to determine contrast ratio" } } diff --git a/test/checks/color/color-contrast.js b/test/checks/color/color-contrast.js index 542f2ef3bb..e9bde325a2 100644 --- a/test/checks/color/color-contrast.js +++ b/test/checks/color/color-contrast.js @@ -3,9 +3,11 @@ describe('color-contrast', function() { var fixture = document.getElementById('fixture'); var fixtureSetup = axe.testUtils.fixtureSetup; + var checkSetup = axe.testUtils.checkSetup; var shadowSupported = axe.testUtils.shadowSupport.v1; var shadowCheckSetup = axe.testUtils.shadowCheckSetup; var checkContext = axe.testUtils.MockCheckContext(); + var contrastEvaluate = checks['color-contrast'].evaluate; afterEach(function() { fixture.innerHTML = ''; @@ -14,14 +16,14 @@ describe('color-contrast', function() { }); it('should return the proper values stored in data', function() { - fixture.innerHTML = - '
' + - 'My text
'; - var target = fixture.querySelector('#target'); - - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + var params = checkSetup( + '
' + + 'My text
' + ); var white = new axe.commons.color.Color(255, 255, 255, 1); var black = new axe.commons.color.Color(0, 0, 0, 1); + + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.equal(checkContext._data.bgColor, white.toHexString()); assert.equal(checkContext._data.fgColor, black.toHexString()); assert.equal(checkContext._data.contrastRatio, '21.00'); @@ -31,148 +33,138 @@ describe('color-contrast', function() { }); it('should return true when there is sufficient contrast because of bold tag', function() { - fixture.innerHTML = - '
' + - 'My text
'; - var target = fixture.querySelector('#target'); + var params = checkSetup( + '
' + + 'My text
' + ); - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return true when there is sufficient contrast because of font weight', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - 'My text
'; - var target = fixture.querySelector('#target'); - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + 'My text' + ); + + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return true when there is sufficient contrast because of font size', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - 'My text
'; - var target = fixture.querySelector('#target'); - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + 'My text' + ); + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return false when there is not sufficient contrast because of font size', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - 'My text
'; - var target = fixture.querySelector('#target'); - assert.isFalse( - checks['color-contrast'].evaluate.call(checkContext, target) + 'My text' ); - assert.deepEqual(checkContext._relatedNodes, [target]); + + assert.isFalse(contrastEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._relatedNodes, [params[0]]); }); it('should return true when there is sufficient contrast with explicit transparency', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - 'My text
'; - var target = fixture.querySelector('#target'); + 'My text' + ); - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return true when there is sufficient contrast with implicit transparency', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - 'My text
'; - var target = fixture.querySelector('#target'); + 'My text' + ); - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return true when there is sufficient contrast', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - 'My text
'; - var target = fixture.querySelector('#target'); - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + 'My text' + ); + + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return true for inline elements with sufficient contrast spanning multiple lines', function() { - fixture.innerHTML = - '

Text oh heyyyy and here\'s
a link

'; - var target = fixture.querySelector('#target'); + var params = checkSetup( + '

Text oh heyyyy and here\'s
a link

' + ); if (window.PHANTOMJS) { assert.ok('PhantomJS is a liar'); } else { - assert.isTrue( - checks['color-contrast'].evaluate.call(checkContext, target) - ); + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); } }); it('should return undefined for inline elements spanning multiple lines that are overlapped', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - '

Text oh heyyyy and here\'s
a link

'; - var target = fixture.querySelector('#target'); - assert.isUndefined( - checks['color-contrast'].evaluate.call(checkContext, target) + '

Text oh heyyyy and here\'s
a link

' ); + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return true for inline elements with sufficient contrast', function() { - fixture.innerHTML = - '

Text oh heyyyy and here\'s bold text

'; - var target = fixture.querySelector('#target'); - var result = checks['color-contrast'].evaluate.call(checkContext, target); - assert.isTrue(result); + var params = checkSetup( + '

Text oh heyyyy and here\'s bold text

' + ); + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return false when there is not sufficient contrast', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - 'My text
'; - var target = fixture.querySelector('#target'); - assert.isFalse( - checks['color-contrast'].evaluate.call(checkContext, target) + 'My text' ); - assert.deepEqual(checkContext._relatedNodes, [target]); + + assert.isFalse(contrastEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._relatedNodes, [params[0]]); }); it('should ignore position:fixed elements above the target', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - '
header
' + - '
' + - 'stuff This is some text' + - '
' + - '
'; - var target = fixture.querySelector('#target'); - var expectedRelatedNodes = fixture.querySelector('#background'); - assert.isFalse( - checks['color-contrast'].evaluate.call(checkContext, target) + '
header
' + + '
' + + 'stuff This is some text' + + '
' + + '' ); + var expectedRelatedNodes = fixture.querySelector('#background'); + assert.isFalse(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, [expectedRelatedNodes]); }); it('should find contrast issues on position:fixed elements', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - '
header
' + - '
' + - 'stuff This is some text' + - '
' + - '
'; - - var target = fixture.querySelector('#target'); - assert.isFalse( - checks['color-contrast'].evaluate.call(checkContext, target) + '
header
' + + '
' + + 'stuff This is some text' + + '
' + + '' ); - assert.deepEqual(checkContext._relatedNodes, [target]); + assert.isFalse(contrastEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._relatedNodes, [params[0]]); }); it('should return undefined for background-image elements', function() { @@ -182,32 +174,28 @@ describe('color-contrast', function() { 'ABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKU' + 'E1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'; - fixture.innerHTML = + var params = checkSetup( '
' + - '

Text 1

' + - '
'; - - var target = fixture.querySelector('#target'); - assert.isUndefined( - checks['color-contrast'].evaluate.call(checkContext, target) + dataURI + + ') no-repeat left center; padding: 5px 0 5px 25px;">' + + '

Text 1

' + + '' ); + + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.isUndefined(checkContext._data.bgColor); assert.equal(checkContext._data.contrastRatio, 0); assert.equal(checkContext._data.missingData, 'bgImage'); }); it('should return undefined for background-gradient elements', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - '

Text 2

' + - '
'; - - var target = fixture.querySelector('#target'); - assert.isUndefined( - checks['color-contrast'].evaluate.call(checkContext, target) + '

Text 2

' + + '' ); + + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.isUndefined(checkContext._data.bgColor); assert.equal(checkContext._data.missingData, 'bgGradient'); assert.equal(checkContext._data.contrastRatio, 0); @@ -216,11 +204,12 @@ describe('color-contrast', function() { it('should return undefined when there are elements overlapping', function(done) { // Give Edge time to scroll... :/ setTimeout(function() { - fixture.innerHTML = + var params = checkSetup( '
' + - 'My text
'; - var target = fixture.querySelector('#target'); - var result = checks['color-contrast'].evaluate.call(checkContext, target); + 'My text
' + ); + + var result = contrastEvaluate.apply(checkContext, params); assert.isUndefined(result); assert.equal(checkContext._data.missingData, 'bgOverlap'); assert.equal(checkContext._data.contrastRatio, 0); @@ -229,10 +218,10 @@ describe('color-contrast', function() { }); it('should return true when a form wraps mixed content', function() { - fixture.innerHTML = - '

Some text

'; - var target = fixture.querySelector('#pass6'); - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + var params = checkSetup( + '

Some text

' + ); + assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); it('should return true when a label wraps a text input', function() { @@ -242,54 +231,47 @@ describe('color-contrast', function() { if (window.PHANTOMJS) { assert.ok('PhantomJS is a liar'); } else { - var result = checks['color-contrast'].evaluate.call( - checkContext, - target, - {}, - virtualNode - ); + var result = contrastEvaluate.call(checkContext, target, {}, virtualNode); assert.isTrue(result); } }); it("should return true when a label wraps a text input but doesn't overlap", function() { - fixture.innerHTML = + var params = checkSetup( ''; - var target = fixture.querySelector('#target'); - var result = checks['color-contrast'].evaluate.call(checkContext, target); + 'My text ' + ); + var result = contrastEvaluate.apply(checkContext, params); assert.isTrue(result); }); it('should return true when there is sufficient contrast based on thead', function() { - fixture.innerHTML = - '
Col 1
'; - var target = fixture.querySelector('#target'); - axe._tree = axe.utils.getFlattenedTree(fixture.firstChild); - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + var params = checkSetup( + '
Col 1
' + ); + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return true when there is sufficient contrast based on tbody', function() { - fixture.innerHTML = - '
Col 1
'; - var target = fixture.querySelector('#target'); - axe._tree = axe.utils.getFlattenedTree(fixture.firstChild); - assert.isTrue(checks['color-contrast'].evaluate.call(checkContext, target)); + var params = checkSetup( + '
Col 1
' + ); + assert.isTrue(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._relatedNodes, []); }); it('should return undefined if element overlaps text content', function(done) { // Give Edge time to scroll setTimeout(function() { - fixture.innerHTML = + var params = checkSetup( '
' + - '
Hi
' + - '
' + - '
'; - var target = fixture.querySelector('#target'); - var actual = checks['color-contrast'].evaluate.call(checkContext, target); - assert.isUndefined(actual); + '
Hi
' + + '
' + + '' + ); + + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.equal(checkContext._data.missingData, 'bgOverlap'); assert.equal(checkContext._data.contrastRatio, 0); done(); @@ -297,13 +279,13 @@ describe('color-contrast', function() { }); it('should return undefined if element has same color as background', function() { - fixture.innerHTML = + var params = checkSetup( '
' + - '
Text
' + - '
'; - var target = fixture.querySelector('#target'); - var actual = checks['color-contrast'].evaluate.call(checkContext, target); - assert.isUndefined(actual); + '
Text
' + + '' + ); + + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.equal(checkContext._data.missingData, 'equalRatio'); assert.equal(checkContext._data.contrastRatio, 1); }); @@ -315,24 +297,44 @@ describe('color-contrast', function() { 'ABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKU' + 'E1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'; - fixture.innerHTML = + var params = checkSetup( '
' + - '

Text 1

' + - '
'; - - var target = fixture.querySelector('#target'); - assert.isUndefined( - checks['color-contrast'].evaluate.call(checkContext, target) + dataURI + + ') no-repeat left center; padding: 5px 0 5px 25px;">' + + '

Text 1

' + + '' ); + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.equal( checkContext._relatedNodes[0], document.querySelector('#background') ); }); + it('should return undefined for a single character text with insufficient contrast', function() { + var params = checkSetup( + '
' + + '
X
' + + '
' + ); + + var actual = contrastEvaluate.apply(checkContext, params); + assert.isUndefined(actual); + assert.equal(checkContext._data.missingData, 'shortTextContent'); + }); + + it('should return true for a single character text with insufficient contrast', function() { + var params = checkSetup( + '
' + + '
X
' + + '
' + ); + + var actual = contrastEvaluate.apply(checkContext, params); + assert.isTrue(actual); + }); + (shadowSupported ? it : xit)( 'returns colors across Shadow DOM boundaries', function() { @@ -341,10 +343,7 @@ describe('color-contrast', function() { '

Text

' ); var container = fixture.querySelector('#container'); - var result = checks['color-contrast'].evaluate.apply( - checkContext, - params - ); + var result = contrastEvaluate.apply(checkContext, params); assert.isFalse(result); assert.deepEqual(checkContext._relatedNodes, [container]); }