How to Test CSS Font Loading API Using Jest

Learn how to simulate and validate font loading processes in Jest, ensuring smooth performance in web development with efficient testing methods.
Jun 20 2024 · 3 min read

Introduction

Consider a scenario where you want to add dynamic fonts to your website. Here dynamic fonts mean, they should load conditionally or can come from the API response. You are not able to add them directly using the @font-face CSS selector. 

In this case, The CSS Font Loading API will be useful to load and manage custom fonts in your web application using FontFace

In this blog post, we’ll explore how to use CSS Font Loading API for custom fonts in typescript and write Jest tests for this.

Stop the habit of wishful thinking and start the habit of thoughtful wishes with Justly.


Load custom fonts using Font-loading API

Fonts have two main properties, family(i.e. Roboto) and style(i.e. Bold, Light) and their files. Below may be the structure of the fonts,

type Font = {
  family: string;
  style: string;
  file: string;
};

Suppose you have a fonts array like below, 

const fonts: Font[] = [
  {
    family: 'Roboto',
    style: 'Regular',
    file: 'Roboto-Regular.ttf',
  },
  {
    family: 'Roboto',
    style: 'Bold',
    file: 'Roboto-Bold.ttf',
  },
]

Useful entities while working with fonts, 

  • FontFace constructor: Equivalent to @font-face. Use to init fonts on web apps.
  • FontFaceSet worker: Manages font-face loading and querying their download status.
  • document.font: Set of Fonts loaded on the browser

We can use them like below,

export const loadFonts = async (fonts: Font[]): Promise<FontFaceSet> => {

  // get existing fonts from document to avoid multiple loading
  const existingFonts = new Set(
    Array.from(document.fonts.values()).map(
      (fontFace) => fontFace.family
    )
  );

  // append pending fonts to document
  fonts.forEach((font) => {
    const name = `${font.family}-${font.style}`;  // Roboto-medium

    // Return if font is already loaded
    if (existingFonts.has(name)) return;

    // Initialize FontFace
    const fontFace = new FontFace(name, `url(${font.file})`);
    document.fonts.add(fontFace);  // prepare FontFaceSet
  });

  // returns promise of FontFaceSet
  return document.fonts.ready.then();
}

The FontFaceSet promise will resolve when the document has completed loading fonts, and no further font loads are needed.

That’s it.

This is the easiest way to load custom fonts.

FontFace Test

While it is easy to manage fonts using API, it’s crucial to ensure their proper functioning through testing as we don’t have a browser environment while running tests and it will throw errors. 

Let’s try to write a jest test without mocking the browser environment, 

describe('loadFonts', () => {
  it('should not add fonts that already exist in the document', async () => {
     await utils.loadFonts(fonts);
     expect(document.fonts.add).not.toHaveBeenCalled();
  });

  it('should load new fonts into the document', async () => {
     document.fonts.values = jest.fn(() => [] as any);
     await utils.loadFonts(fonts);
     expect(document.fonts.add).toHaveBeenCalled();
   });
});

It is throwing errors like below. Here undefined means document.fonts

TypeError: Cannot read properties of undefined (reading 'values')

Let’s mock document.fonts as they will not be available in the jest environment. First, create an object of the FontFaceSet and add the required properties to it.

// Mock FontFaceSet
 const mockFontFaceSet = {
   add: jest.fn(),   // require for adding fonts to document.font set
   ready: Promise.resolve(), // require for managinf font loading
   values: jest.fn(() => [ // returns existing fonts
     { family: 'Roboto-Regular' },
     { family: 'Roboto-Bold' }
   ])
 };

Then define the document.fonts object,

Object.defineProperty(document, 'fonts', {
    value: mockFontFaceSet,
});

Now, when there is a document.fonts instance comes while running tests, jest will use this as document.fonts , which returns mockFontFaceSet .

Rewrite the above tests,

describe('loadFonts', () => {
   it('should not add fonts that already exist in the document', async () => {
     await utils.loadFonts(fonts);
     expect(document.fonts.add).not.toHaveBeenCalled();
   });

   it('should load new fonts into the document', async () => {
     document.fonts.values = jest.fn(() => [] as any);
     await utils.loadFonts(fonts);
     expect(document.fonts.add).toHaveBeenCalled();
   });
 });

We will get an error ReferenceError: FontFace is not defined for a second test case, as FontFace is also not available without a browser.

Here is the solution for defining FontFace in jest.setup.ts file.

(global as any).FontFace = class {
  constructor(public family?: string, public source?: string) { }
};

By doing this, now FontFace is available to jest environment with same functionalities of FontFace constructor of Font loading API

Conclusion

The browser environment will not be available on the server or in test environments. For smooth operation, we need to create a replica of the browser instance. 

In jest, We can define custom variables and mock browser environments. You can use the same approach for mocking other browser properties like location, or navigator .

We’re Grateful to have you with us on this journey!

Suggestions and feedback are more than welcome! 

Please reach us at Canopas Twitter handle @canopas_eng with your content or feedback. Your input enriches our content and fuels our motivation to create more valuable and informative articles for you.

Similar Articles


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.


Talk to an expert
get intouch
Our team is happy to answer your questions. Fill out the form and we’ll get back to you as soon as possible
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.