Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(color-contrast): ignore zero width characters #4103

Merged
merged 4 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions lib/commons/text/is-icon-ligature.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
straker marked this conversation as resolved.
Show resolved Hide resolved
}

// ensure font meets the 30px width requirement (30px font-size doesn't
// necessarily mean its 30px wide when drawn)
if (width < 30) {
Expand Down
Binary file added test/assets/ZeroWidth0Char.woff
Binary file not shown.
118 changes: 67 additions & 51 deletions test/commons/text/is-icon-ligature.js
Original file line number Diff line number Diff line change
@@ -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('<div id="target">Normal text</div>');
it('should return false for normal text', () => {
const target = queryFixture('<div id="target">Normal text</div>');
assert.isFalse(isIconLigature(target.children[0]));
});

it('should return false for emoji', function () {
var target = queryFixture('<div id="target">🌎</div>');
it('should return false for emoji', () => {
const target = queryFixture('<div id="target">🌎</div>');
assert.isFalse(isIconLigature(target.children[0]));
});

it('should return false for non-bmp unicode', function () {
var target = queryFixture('<div id="target">◓</div>');
it('should return false for non-bmp unicode', () => {
const target = queryFixture('<div id="target">◓</div>');
assert.isFalse(isIconLigature(target.children[0]));
});

it('should return false for whitespace strings', function () {
var target = queryFixture('<div id="target"> </div>');
it('should return false for whitespace strings', () => {
const target = queryFixture('<div id="target"> </div>');
assert.isFalse(isIconLigature(target.children[0]));
});

(fontApiSupport ? it : it.skip)(
'should return false for common ligatures (fi)',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: Roboto">figure</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -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(
'<div id="target" style="font-family: Roboto">ffugative</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -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(
'<div id="target" style="font-family: Roboto">flu shot</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -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(
'<div id="target" style="font-family: Roboto">ffigure</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -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(
'<div id="target" style="font-family: Roboto">fflu shot</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -110,35 +116,45 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should return true for an icon ligature',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'Material Icons\'">delete</div>'
);
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(
'<div id="target" style="font-family: Roboto"> fflu shot </div>'
);
assert.isFalse(isIconLigature(target.children[0]));
});

(fontApiSupport ? it : it.skip)(
'should return true for a font that has no character data',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'Material Icons\'">f</div>'
);
assert.isTrue(isIconLigature(target.children[0]));
}
);

(fontApiSupport ? it : it.skip)(
'should return true for a font that has zero width characters',
() => {
const target = queryFixture(
'<div id="target" style="font-family: \'ZeroWidth0Char\'">0</div>'
);
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(
'<div id="target" style="font-family: Fira Code">!==</div>'
);
assert.isFalse(isIconLigature(target.children[0]));
Expand All @@ -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(
'<div id="target" style="font-family: \'Material Icons\'">keyboard_arrow_left</div>'
);
assert.isTrue(isIconLigature(target.children[0]));
Expand All @@ -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(
'<div id="target" style="font-family: \'LigatureSymbols\'">delete</div>'
);

Expand All @@ -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(
'<div id="target" style="font-family: \'Roboto\'">__non-icon text__</div>'
);

Expand All @@ -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(
'<div id="target" style="font-family: \'LigatureSymbols\'">delete</div>'
);

Expand All @@ -204,20 +220,20 @@ describe('text.isIconLigature', function () {

(fontApiSupport ? it : it.skip)(
'should allow lower percent (will flag text ligatures)',
function () {
var target = queryFixture(
() => {
const target = queryFixture(
'<div id="target" style="font-family: Roboto">figure</div>'
);
assert.isTrue(isIconLigature(target.children[0], 0));
}
);
});

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(
'<div id="target" style="font-family: \'LigatureSymbols\'">delete</div>'
);

Expand Down
Loading