From 4deb0a0876d574c3d7d586b27ae07d4f5be586db Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Mon, 7 Aug 2023 13:41:01 -0600 Subject: [PATCH] fix(color-contrast): ignore zero width characters (#4103) * fix(color-contrast): ignore zero width characters * fix * replace affirm font with purpose built zero width 0 char font * Update lib/commons/text/is-icon-ligature.js Co-authored-by: Dan Bjorge --------- Co-authored-by: Dan Bjorge --- lib/commons/text/is-icon-ligature.js | 16 ++-- test/assets/ZeroWidth0Char.woff | Bin 0 -> 976 bytes test/commons/text/is-icon-ligature.js | 118 +++++++++++++++----------- 3 files changed, 77 insertions(+), 57 deletions(-) create mode 100644 test/assets/ZeroWidth0Char.woff 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 0000000000000000000000000000000000000000..3c26a6e1ed621aaf8817a01caf6836c6b069aedf GIT binary patch literal 976 zcmXT-cXRU(3GruOV7|b>#Q+4XM;I7EG$wF%b5j7SV*$!61LE}yCIxr7xrO)w#kK(X zG9bM0$kJK<>US!a%I>e+Pq6#hm1Xl!OF^M=GJ~Hh=tM7B9JD zW;}zdgj`MaYLBeBIS&CfLDh%-$Ui7>l!3X2XGH@igHaIUPM{nE zLjcfth?>Mdhb$T!r#3dO?9Sw8W_IRpNvk=KaN-1uNNPfAS_7jSn~K1MXR{7nIFJB@ zYd+j>Qsi>(WfCxAVhD=jVg=gM+P^>RP=LhI^Su`*EoE)*D{ANCQoD1+abi%H*Ncdj zU8NjqM?5@o_V8VDe{tjc3+bPFww>GcWY0W({O{e*e{&gj9OY*@7tinC#wt@Iv8|w4 z|FEvjgOnR>%I|o$7pl+WEnjhbjmWmc*OM=YZgvoV-=_QSugRT5-1~TMzu-DuAZ@D% zRIxv)Psmy(P|>-I*F@!4MDhXahNKpWmzrNr6j1agvab){r2{=Ef$lf$@{X!5T3(R)w=nEeiV%{{JuV z>A&j=FE%$bHi^j@4y%40uz-I&i literal 0 HcmV?d00001 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
' );