How to Test CSS Font Loading API Using Jest
Tags: webdev, css, programming, api
Published: 2024-06-21 10:26:02

> Exciting News! Our blog has a new **[home](https://canopas.com/blog)**!🚀

## 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](https://developer.mozilla.org/en-US/docs/Web/API/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](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API) for custom fonts in typescript and write [Jest](https://jestjs.io/docs/getting-started) 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](https://canopas.com/blog)**.

> To read the full version, please visit **[this blog](https://canopas.com/how-to-test-css-font-loading-api-using-jest-63be55b6b167)**.

---

If you like what you read, be sure to hit 💖 button! — 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! 👋