How to Test CSS Font Loading API Using Jest

Simulating and Validating Font Loading Processes in jest

·

4 min read

How to Test CSS Font Loading API Using Jest

Exciting News! Our blog has a new home!🚀

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.

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 .

That’s it for today. Keep exploring for the best!!

This blog post was originally published on canopas.com.

To read the full version, please visit this blog.


If you like what you read, be sure to hit 💖 button below! — as a writer it means the world!

I encourage you to share your thoughts in the comments section below. Your input not only enriches our content but also fuels our motivation to create more valuable and informative articles for you.

Happy coding! 👋

Did you find this article valuable?

Support Canopas's blog by becoming a sponsor. Any amount is appreciated!