From 05e4c082edb94de4560efacf59d260993a0748b4 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 3 Apr 2026 12:26:23 +0300 Subject: [PATCH] feat(appliedFontsStore): move font loading logic into loadFont function and cover it with tests --- .../appliedFontsStore.svelte.ts | 20 ++-- .../store/appliedFontsStore/utils/index.ts | 1 + .../utils/loadFont/loadFont.test.ts | 92 +++++++++++++++++++ .../utils/loadFont/loadFont.ts | 26 ++++++ 4 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.test.ts create mode 100644 src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.ts diff --git a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts index ef2bacb..62cda8d 100644 --- a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts +++ b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts @@ -1,10 +1,11 @@ import { SvelteMap } from 'svelte/reactivity'; -import type { - FontLoadRequestConfig, - FontLoadStatus, +import { + type FontLoadRequestConfig, + type FontLoadStatus, } from '../../types'; import { getEffectiveConcurrency, + loadFont, yieldToMainThread, } from './utils'; @@ -184,17 +185,12 @@ export class AppliedFontsManager { for (const [key, config] of entries) { const buffer = buffers.get(key); - if (!buffer) continue; + if (!buffer) { + continue; + } try { - const weightRange = config.isVariable ? '100 900' : `${config.weight}`; - const font = new FontFace(config.name, buffer, { - weight: weightRange, - style: 'normal', - display: 'swap', - }); - await font.load(); - document.fonts.add(font); + const font = await loadFont(config, buffer); this.#loadedFonts.set(key, font); this.#buffersByUrl.set(config.url, buffer); this.#urlByKey.set(key, config.url); diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/index.ts b/src/entities/Font/model/store/appliedFontsStore/utils/index.ts index c16a74b..bf35e55 100644 --- a/src/entities/Font/model/store/appliedFontsStore/utils/index.ts +++ b/src/entities/Font/model/store/appliedFontsStore/utils/index.ts @@ -1,2 +1,3 @@ export { getEffectiveConcurrency } from './getEffectiveConcurrency/getEffectiveConcurrency'; +export { loadFont } from './loadFont/loadFont'; export { yieldToMainThread } from './yieldToMainThread/yieldToMainThread'; diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.test.ts b/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.test.ts new file mode 100644 index 0000000..22dfcbd --- /dev/null +++ b/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.test.ts @@ -0,0 +1,92 @@ +/** @vitest-environment jsdom */ +import { loadFont } from './loadFont'; + +describe('loadFont', () => { + let mockFontInstance: any; + let mockFontFaceSet: { add: ReturnType; delete: ReturnType }; + + beforeEach(() => { + mockFontFaceSet = { add: vi.fn(), delete: vi.fn() }; + Object.defineProperty(document, 'fonts', { value: mockFontFaceSet, configurable: true, writable: true }); + + const MockFontFace = vi.fn( + function(this: any, name: string, buffer: BufferSource, options: FontFaceDescriptors) { + this.name = name; + this.buffer = buffer; + this.options = options; + this.load = vi.fn().mockResolvedValue(this); + mockFontInstance = this; + }, + ); + vi.stubGlobal('FontFace', MockFontFace); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('constructs FontFace with exact weight for static fonts', async () => { + const buffer = new ArrayBuffer(8); + await loadFont({ name: 'Roboto', weight: 400 }, buffer); + + expect(FontFace).toHaveBeenCalledWith('Roboto', buffer, expect.objectContaining({ weight: '400' })); + }); + + it('constructs FontFace with weight range for variable fonts', async () => { + const buffer = new ArrayBuffer(8); + await loadFont({ name: 'Roboto', weight: 400, isVariable: true }, buffer); + + expect(FontFace).toHaveBeenCalledWith('Roboto', buffer, expect.objectContaining({ weight: '100 900' })); + }); + + it('sets style: normal and display: swap on FontFace options', async () => { + await loadFont({ name: 'Lato', weight: 700 }, new ArrayBuffer(8)); + + expect(FontFace).toHaveBeenCalledWith( + 'Lato', + expect.anything(), + expect.objectContaining({ style: 'normal', display: 'swap' }), + ); + }); + + it('passes the buffer as the second argument to FontFace', async () => { + const buffer = new ArrayBuffer(16); + await loadFont({ name: 'Inter', weight: 400 }, buffer); + + expect(FontFace).toHaveBeenCalledWith('Inter', buffer, expect.anything()); + }); + + it('calls font.load() and adds the font to document.fonts', async () => { + const buffer = new ArrayBuffer(8); + const result = await loadFont({ name: 'Inter', weight: 400 }, buffer); + + expect(mockFontInstance.load).toHaveBeenCalledOnce(); + expect(mockFontFaceSet.add).toHaveBeenCalledWith(mockFontInstance); + expect(result).toBe(mockFontInstance); + }); + + it('propagates and logs error when font.load() rejects', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const loadError = new Error('parse failed'); + const MockFontFace = vi.fn( + function(this: any, name: string, buffer: BufferSource, options: FontFaceDescriptors) { + this.load = vi.fn().mockRejectedValue(loadError); + }, + ); + vi.stubGlobal('FontFace', MockFontFace); + + await expect(loadFont({ name: 'Broken', weight: 400 }, new ArrayBuffer(8))).rejects.toThrow('parse failed'); + expect(consoleSpy).toHaveBeenCalledWith(loadError); + }); + + it('propagates and logs error when document.fonts.add throws', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const addError = new Error('add failed'); + mockFontFaceSet.add.mockImplementation(() => { + throw addError; + }); + + await expect(loadFont({ name: 'Broken', weight: 400 }, new ArrayBuffer(8))).rejects.toThrow('add failed'); + expect(consoleSpy).toHaveBeenCalledWith(addError); + }); +}); diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.ts b/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.ts new file mode 100644 index 0000000..71839f6 --- /dev/null +++ b/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.ts @@ -0,0 +1,26 @@ +import type { FontLoadRequestConfig } from '../../../../types'; + +export type PartialConfig = Pick; +/** + * Loads a font from a buffer and adds it to the document's font collection. + * @param config - The font load request configuration. + * @param buffer - The buffer containing the font data. + * @returns A promise that resolves to the loaded `FontFace`. + */ +export async function loadFont(config: PartialConfig, buffer: BufferSource): Promise { + try { + const weightRange = config.isVariable ? '100 900' : `${config.weight}`; + const font = new FontFace(config.name, buffer, { + weight: weightRange, + style: 'normal', + display: 'swap', + }); + await font.load(); + document.fonts.add(font); + + return font; + } catch (error) { + console.error(error); + throw error; + } +}