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
'
);