diff --git a/lib/commons/text/is-icon-ligature.js b/lib/commons/text/is-icon-ligature.js index 7924b0bbc9..428117f0de 100644 --- a/lib/commons/text/is-icon-ligature.js +++ b/lib/commons/text/is-icon-ligature.js @@ -93,11 +93,7 @@ export default function isIconLigature( // keep track of each font encountered and the number of times it shows up // as a ligature. - if (!cache.get('fonts')) { - cache.set('fonts', {}); - } - const fonts = cache.get('fonts'); - + const fonts = cache.get('fonts', () => ({})); const style = window.getComputedStyle(textVNode.parent.actualNode); const fontFamily = style.getPropertyValue('font-family'); @@ -109,7 +105,7 @@ export default function isIconLigature( } const font = fonts[fontFamily]; - // improve the performance by only comparing the image data of a fon a certain number of times + // improve the performance by only comparing the image data of a font a certain number of times // NOTE: This MIGHT cause an issue if someone uses an icon font to render actual text. // We're leaving this as-is, unless someone reports a false positive over it. if (font.occurrences >= occurrenceThreshold) { @@ -143,6 +139,14 @@ export default function isIconLigature( const firstChar = nodeValue.charAt(0); let width = canvasContext.measureText(firstChar).width; + // we already checked for typical zero-width unicode formatting characters further up, + // so we assume that any remaining zero-width characters are part of an icon ligature + // @see https://github.com/dequelabs/axe-core/issues/3918 + if (width === 0) { + font.numLigatures++; + return true; + } + // ensure font meets the 30px width requirement (30px font-size doesn't // necessarily mean its 30px wide when drawn) if (width < 30) { diff --git a/test/assets/ZeroWidth0Char.woff b/test/assets/ZeroWidth0Char.woff new file mode 100644 index 0000000000..3c26a6e1ed Binary files /dev/null and b/test/assets/ZeroWidth0Char.woff differ diff --git a/test/commons/text/is-icon-ligature.js b/test/commons/text/is-icon-ligature.js index ee11e2b8eb..2da2ba8ba2 100644 --- a/test/commons/text/is-icon-ligature.js +++ b/test/commons/text/is-icon-ligature.js @@ -1,67 +1,73 @@ -describe('text.isIconLigature', function () { +describe('text.isIconLigature', () => { 'use strict'; - var isIconLigature = axe.commons.text.isIconLigature; - var queryFixture = axe.testUtils.queryFixture; - var fontApiSupport = !!document.fonts; + const isIconLigature = axe.commons.text.isIconLigature; + const queryFixture = axe.testUtils.queryFixture; + const fontApiSupport = !!document.fonts; - before(function (done) { + before(done => { if (!fontApiSupport) { done(); } - var firaFont = new FontFace( + const firaFont = new FontFace( 'Fira Code', 'url(/test/assets/FiraCode-Regular.woff)' ); - var ligatureFont = new FontFace( + const ligatureFont = new FontFace( 'LigatureSymbols', 'url(/test/assets/LigatureSymbols.woff)' ); - var materialFont = new FontFace( + const materialFont = new FontFace( 'Material Icons', 'url(/test/assets/MaterialIcons.woff2)' ); - var robotoFont = new FontFace('Roboto', 'url(/test/assets/Roboto.woff2)'); + const robotoFont = new FontFace('Roboto', 'url(/test/assets/Roboto.woff2)'); + const zeroWidth0CharFont = new FontFace( + 'ZeroWidth0Char', + 'url(/test/assets/ZeroWidth0Char.woff)' + ); window.Promise.all([ firaFont.load(), ligatureFont.load(), materialFont.load(), - robotoFont.load() - ]).then(function () { + robotoFont.load(), + zeroWidth0CharFont.load() + ]).then(() => { document.fonts.add(firaFont); document.fonts.add(ligatureFont); document.fonts.add(materialFont); document.fonts.add(robotoFont); + document.fonts.add(zeroWidth0CharFont); done(); }); }); - it('should return false for normal text', function () { - var target = queryFixture('
Normal text
'); + it('should return false for normal text', () => { + const target = queryFixture('
Normal text
'); assert.isFalse(isIconLigature(target.children[0])); }); - it('should return false for emoji', function () { - var target = queryFixture('
🌎
'); + it('should return false for emoji', () => { + const target = queryFixture('
🌎
'); assert.isFalse(isIconLigature(target.children[0])); }); - it('should return false for non-bmp unicode', function () { - var target = queryFixture('
â—“
'); + it('should return false for non-bmp unicode', () => { + const target = queryFixture('
â—“
'); assert.isFalse(isIconLigature(target.children[0])); }); - it('should return false for whitespace strings', function () { - var target = queryFixture('
'); + it('should return false for whitespace strings', () => { + const target = queryFixture('
'); assert.isFalse(isIconLigature(target.children[0])); }); (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (fi)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
figure
' ); assert.isFalse(isIconLigature(target.children[0])); @@ -70,8 +76,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (ff)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
ffugative
' ); assert.isFalse(isIconLigature(target.children[0])); @@ -80,8 +86,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (fl)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
flu shot
' ); assert.isFalse(isIconLigature(target.children[0])); @@ -90,8 +96,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (ffi)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
ffigure
' ); assert.isFalse(isIconLigature(target.children[0])); @@ -100,8 +106,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false for common ligatures (ffl)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
fflu shot
' ); assert.isFalse(isIconLigature(target.children[0])); @@ -110,16 +116,16 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return true for an icon ligature', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
delete
' ); assert.isTrue(isIconLigature(target.children[0])); } ); - (fontApiSupport ? it : it.skip)('should trim the string', function () { - var target = queryFixture( + (fontApiSupport ? it : it.skip)('should trim the string', () => { + const target = queryFixture( '
fflu shot
' ); assert.isFalse(isIconLigature(target.children[0])); @@ -127,18 +133,28 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return true for a font that has no character data', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
f
' ); assert.isTrue(isIconLigature(target.children[0])); } ); + (fontApiSupport ? it : it.skip)( + 'should return true for a font that has zero width characters', + () => { + const target = queryFixture( + '
0
' + ); + assert.isTrue(isIconLigature(target.children[0])); + } + ); + (fontApiSupport ? it : it.skip)( 'should return false for a programming text ligature', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
!==
' ); assert.isFalse(isIconLigature(target.children[0])); @@ -147,8 +163,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return true for an icon ligature with low pixel difference', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
keyboard_arrow_left
' ); assert.isTrue(isIconLigature(target.children[0])); @@ -157,8 +173,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return true after the 3rd time the font is an icon', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
delete
' ); @@ -174,8 +190,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should return false after the 3rd time the font is not an icon', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
__non-icon text__
' ); @@ -189,11 +205,11 @@ describe('text.isIconLigature', function () { } ); - describe('pixelThreshold', function () { + describe('pixelThreshold', () => { (fontApiSupport ? it : it.skip)( 'should allow higher percent (will not flag icon ligatures)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
delete
' ); @@ -204,8 +220,8 @@ describe('text.isIconLigature', function () { (fontApiSupport ? it : it.skip)( 'should allow lower percent (will flag text ligatures)', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
figure
' ); assert.isTrue(isIconLigature(target.children[0], 0)); @@ -213,11 +229,11 @@ describe('text.isIconLigature', function () { ); }); - describe('occurrenceThreshold', function () { + describe('occurrenceThreshold', () => { (fontApiSupport ? it : it.skip)( 'should change the number of times a font is seen before returning', - function () { - var target = queryFixture( + () => { + const target = queryFixture( '
delete
' );