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! โ 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! ๐