diff --git a/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.test.ts b/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.test.ts new file mode 100644 index 0000000..ef196d4 --- /dev/null +++ b/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.test.ts @@ -0,0 +1,111 @@ +import { + describe, + expect, + it, +} from 'vitest'; +import type { UnifiedFont } from '../../model/types'; +import { createFontLoadRequestContfig } from './createFontLoadRequestContfig'; + +/** + * Minimal UnifiedFont mock — override only the fields a case exercises. + */ +function createMockFont(overrides: Partial = {}): UnifiedFont { + const baseFont: UnifiedFont = { + id: 'test-font', + name: 'Test Font', + provider: 'google', + category: 'sans-serif', + subsets: ['latin'], + variants: [], + styles: {}, + metadata: { + cachedAt: Date.now(), + }, + features: { + isVariable: false, + tags: [], + }, + }; + + return { ...baseFont, ...overrides }; +} + +describe('createFontLoadRequestContfig', () => { + it('builds a single-element config when a URL resolves', () => { + const font = createMockFont({ + id: 'roboto', + name: 'Roboto', + styles: { variants: { '400': 'https://example.com/roboto-400.woff2' } }, + }); + + const result = createFontLoadRequestContfig(font, 400); + + expect(result).toEqual([ + { + id: 'roboto', + name: 'Roboto', + weight: 400, + url: 'https://example.com/roboto-400.woff2', + isVariable: false, + }, + ]); + }); + + it('returns an empty array when no URL resolves (flatMap drops the font)', () => { + const font = createMockFont({ styles: {} }); + + expect(createFontLoadRequestContfig(font, 400)).toEqual([]); + }); + + it('forwards isVariable from font features', () => { + const font = createMockFont({ + features: { isVariable: true, tags: [] }, + styles: { variants: { '700': 'https://example.com/inter-vf.woff2' } }, + }); + + const [config] = createFontLoadRequestContfig(font, 700); + + expect(config.isVariable).toBe(true); + }); + + it('sets isVariable to undefined when features is absent', () => { + // features is non-optional on UnifiedFont, but upstream data can be partial — + // the optional chain must not throw, and isVariable stays undefined. + const font = createMockFont({ + styles: { variants: { '400': 'https://example.com/font.woff2' } }, + }); + // @ts-expect-error — deliberately drop the guaranteed field to exercise the optional chain + font.features = undefined; + + const [config] = createFontLoadRequestContfig(font, 400); + + expect(config.isVariable).toBeUndefined(); + }); + + it('uses the resolved fallback URL, not just exact matches', () => { + // getFontUrl falls back to styles.regular when the exact weight is missing; + // the config must carry whatever URL actually resolved. + const font = createMockFont({ + styles: { regular: 'https://example.com/font-regular.woff2' }, + }); + + const [config] = createFontLoadRequestContfig(font, 900); + + expect(config.url).toBe('https://example.com/font-regular.woff2'); + expect(config.weight).toBe(900); + }); + + it('carries the requested weight even when the URL is a shared fallback', () => { + const font = createMockFont({ + styles: { variants: { '400': 'https://example.com/shared.woff2' } }, + }); + + expect(createFontLoadRequestContfig(font, 700)[0].weight).toBe(700); + }); + + it('propagates the invalid-weight error from getFontUrl', () => { + const font = createMockFont(); + + expect(() => createFontLoadRequestContfig(font, 450)).toThrow('Invalid weight: 450'); + }); +}); diff --git a/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.ts b/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.ts new file mode 100644 index 0000000..b0aad4c --- /dev/null +++ b/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.ts @@ -0,0 +1,33 @@ +import type { + FontLoadRequestConfig, + UnifiedFont, +} from '../../model'; +import { getFontUrl } from '../getFontUrl/getFontUrl'; + +/** + * Build the font-lifecycle load request for a single font at a given weight. + * + * Returns a 0-or-1 element array rather than `FontLoadRequestConfig | undefined` + * so call sites can `flatMap` over a font list — resolve the URL and drop fonts + * that have none in a single pass, with no separate filter step. An empty array + * means the font has no loadable asset for this weight (or its fallbacks) and is + * silently skipped. + * + * `isVariable` is forwarded from the font's features so the lifecycle manager can + * dedupe variable fonts per ID (they load once regardless of weight) while still + * loading static fonts per weight. + * + * @param font - Unified font to load + * @param weight - Numeric weight (100-900) + * @returns Single-element config array, or `[]` when no URL resolves + * @throws Error when weight is outside the valid 100-900 range (propagated from `getFontUrl`) + */ +export function createFontLoadRequestContfig(font: UnifiedFont, weight: number): FontLoadRequestConfig[] { + const url = getFontUrl(font, weight); + + if (!url) { + return []; + } + + return [{ id: font.id, name: font.name, weight, url, isVariable: font.features?.isVariable }]; +} diff --git a/src/entities/Font/model/index.ts b/src/entities/Font/model/index.ts index 8749b29..36cbda2 100644 --- a/src/entities/Font/model/index.ts +++ b/src/entities/Font/model/index.ts @@ -1,3 +1,6 @@ export * from './const/const'; + +export { getFontCatalog } from './store'; + export * from './store'; export * from './types'; diff --git a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts index c6d7510..72b3d56 100644 --- a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts +++ b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts @@ -483,8 +483,14 @@ export class FontCatalogStore { } } -export function createFontCatalogStore(params: FontStoreParams = {}): FontCatalogStore { - return new FontCatalogStore(params); +let _catalog: FontCatalogStore | undefined; + +export function getFontCatalog(): FontCatalogStore { + return (_catalog ??= new FontCatalogStore({ limit: 50 })); } -export const fontCatalogStore = new FontCatalogStore({ limit: 50 }); +// test-only reset, so specs don't share a live observer +export function __resetFontCatalog() { + _catalog?.destroy(); + _catalog = undefined; +} diff --git a/src/entities/Font/model/store/index.ts b/src/entities/Font/model/store/index.ts index 52dda36..d0d0024 100644 --- a/src/entities/Font/model/store/index.ts +++ b/src/entities/Font/model/store/index.ts @@ -2,11 +2,9 @@ export * from './fontLifecycleManager/fontLifecycleManager.svelte'; // Paginated catalog -export { - createFontCatalogStore, - FontCatalogStore, - fontCatalogStore, -} from './fontCatalogStore/fontCatalogStore.svelte'; +export { getFontCatalog } from './fontCatalogStore/fontCatalogStore.svelte'; + +export type { FontCatalogStore } from './fontCatalogStore/fontCatalogStore.svelte'; // Batch fetch by IDs (detail-cache seeding) export { FontsByIdsStore } from './fontsByIdsStore/fontsByIdsStore.svelte'; diff --git a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte index 0d21abe..bb71dd8 100644 --- a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte +++ b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte @@ -5,21 +5,18 @@ --> @@ -36,7 +39,7 @@ const responsive = getContext('responsive'); id="sample_set" title="Sample Set" headerTitle="visual_output" - headerSubtitle="items_total: {fontCatalogStore.pagination.total ?? 0}" + headerSubtitle="items_total: {total ?? 0}" headerAction={registerAction} > {#snippet headerContent()}