import type { FontshareFont } from '$entities/Font'; import type { GoogleFontItem } from '$entities/Font/api/googleFonts'; import type { UnifiedFont } from '$entities/Font/api/normalize'; import { normalizeFontshareFont, normalizeFontshareFonts, normalizeGoogleFont, normalizeGoogleFonts, } from '$entities/Font/api/normalize'; import { describe, expect, it, } from 'vitest'; describe('Font Normalization', () => { describe('normalizeGoogleFont', () => { const mockGoogleFont: GoogleFontItem = { family: 'Roboto', category: 'sans-serif', variants: ['regular', '700', 'italic', '700italic'], subsets: ['latin', 'latin-ext'], files: { regular: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2', '700': 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu72xWUlvAx05IsDqlA.woff2', italic: 'https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1Mu51xIIzI.woff2', '700italic': 'https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TzBic6CsQ.woff2', }, version: 'v30', lastModified: '2022-01-01', menu: 'https://fonts.googleapis.com/css2?family=Roboto', }; it('normalizes Google Font to unified model', () => { const result = normalizeGoogleFont(mockGoogleFont); expect(result.id).toBe('Roboto'); expect(result.name).toBe('Roboto'); expect(result.provider).toBe('google'); expect(result.category).toBe('sans-serif'); }); it('maps font variants correctly', () => { const result = normalizeGoogleFont(mockGoogleFont); expect(result.variants).toEqual(['regular', '700', 'italic', '700italic']); }); it('maps subsets correctly', () => { const result = normalizeGoogleFont(mockGoogleFont); expect(result.subsets).toContain('latin'); expect(result.subsets).toContain('latin-ext'); expect(result.subsets).toHaveLength(2); }); it('maps style URLs correctly', () => { const result = normalizeGoogleFont(mockGoogleFont); expect(result.styles.regular).toBeDefined(); expect(result.styles.bold).toBeDefined(); expect(result.styles.italic).toBeDefined(); expect(result.styles.boldItalic).toBeDefined(); }); it('includes metadata', () => { const result = normalizeGoogleFont(mockGoogleFont); expect(result.metadata.cachedAt).toBeDefined(); expect(result.metadata.version).toBe('v30'); expect(result.metadata.lastModified).toBe('2022-01-01'); }); it('marks Google Fonts as non-variable', () => { const result = normalizeGoogleFont(mockGoogleFont); expect(result.features.isVariable).toBe(false); expect(result.features.tags).toEqual([]); }); it('handles sans-serif category', () => { const font = { ...mockGoogleFont, category: 'sans-serif' }; const result = normalizeGoogleFont(font); expect(result.category).toBe('sans-serif'); }); it('handles serif category', () => { const font = { ...mockGoogleFont, category: 'serif' }; const result = normalizeGoogleFont(font); expect(result.category).toBe('serif'); }); it('handles display category', () => { const font = { ...mockGoogleFont, category: 'display' }; const result = normalizeGoogleFont(font); expect(result.category).toBe('display'); }); it('handles handwriting category', () => { const font = { ...mockGoogleFont, category: 'handwriting' }; const result = normalizeGoogleFont(font); expect(result.category).toBe('handwriting'); }); it('handles cursive category (maps to handwriting)', () => { const font = { ...mockGoogleFont, category: 'cursive' }; const result = normalizeGoogleFont(font); expect(result.category).toBe('handwriting'); }); it('handles monospace category', () => { const font = { ...mockGoogleFont, category: 'monospace' }; const result = normalizeGoogleFont(font); expect(result.category).toBe('monospace'); }); it('filters invalid subsets', () => { const font = { ...mockGoogleFont, subsets: ['latin', 'latin-ext', 'invalid-subset'], }; const result = normalizeGoogleFont(font); expect(result.subsets).not.toContain('invalid-subset'); expect(result.subsets).toHaveLength(2); }); it('maps variant weights correctly', () => { const font = { ...mockGoogleFont, variants: ['regular', '100', '400', '700', '900'], }; const result = normalizeGoogleFont(font); expect(result.variants).toContain('regular'); expect(result.variants).toContain('100'); expect(result.variants).toContain('400'); expect(result.variants).toContain('700'); expect(result.variants).toContain('900'); }); }); describe('normalizeFontshareFont', () => { const mockFontshareFont: FontshareFont = { id: '20e9fcdc-1e41-4559-a43d-1ede0adc8896', name: 'Satoshi', native_name: null, slug: 'satoshi', category: 'Sans', script: 'latin', publisher: { bio: 'Indian Type Foundry', email: null, id: 'test-id', links: [], name: 'Indian Type Foundry', }, designers: [ { bio: 'Designer bio', links: [], name: 'Designer Name', }, ], related_families: null, display_publisher_as_designer: false, trials_enabled: true, show_latin_metrics: false, license_type: 'itf_ffl', languages: 'Afar, Afrikaans', inserted_at: '2021-03-12T20:49:05Z', story: '
Font story
', version: '1.0', views: 10000, views_recent: 500, is_hot: true, is_new: false, is_shortlisted: false, is_top: true, axes: [], font_tags: [ { name: 'Branding' }, { name: 'Logos' }, ], features: [ { name: 'Alternate t', on_by_default: false, tag: 'ss01', }, ], styles: [ { id: 'style-id-1', default: true, file: '//cdn.fontshare.com/wf/satoshi.woff2', is_italic: false, is_variable: false, properties: {}, weight: { label: 'Regular', name: 'Regular', native_name: null, number: 400, weight: 400, }, }, { id: 'style-id-2', default: false, file: '//cdn.fontshare.com/wf/satoshi-bold.woff2', is_italic: false, is_variable: false, properties: {}, weight: { label: 'Bold', name: 'Bold', native_name: null, number: 700, weight: 700, }, }, { id: 'style-id-3', default: false, file: '//cdn.fontshare.com/wf/satoshi-italic.woff2', is_italic: true, is_variable: false, properties: {}, weight: { label: 'Regular', name: 'Regular', native_name: null, number: 400, weight: 400, }, }, { id: 'style-id-4', default: false, file: '//cdn.fontshare.com/wf/satoshi-bolditalic.woff2', is_italic: true, is_variable: false, properties: {}, weight: { label: 'Bold', name: 'Bold', native_name: null, number: 700, weight: 700, }, }, ], }; it('normalizes Fontshare font to unified model', () => { const result = normalizeFontshareFont(mockFontshareFont); expect(result.id).toBe('satoshi'); expect(result.name).toBe('Satoshi'); expect(result.provider).toBe('fontshare'); expect(result.category).toBe('sans-serif'); }); it('uses slug as unique identifier', () => { const result = normalizeFontshareFont(mockFontshareFont); expect(result.id).toBe('satoshi'); }); it('extracts variant names from styles', () => { const result = normalizeFontshareFont(mockFontshareFont); expect(result.variants).toContain('Regular'); expect(result.variants).toContain('Bold'); expect(result.variants).toContain('Regularitalic'); expect(result.variants).toContain('Bolditalic'); }); it('maps Fontshare Sans to sans-serif category', () => { const font = { ...mockFontshareFont, category: 'Sans' }; const result = normalizeFontshareFont(font); expect(result.category).toBe('sans-serif'); }); it('maps Fontshare Serif to serif category', () => { const font = { ...mockFontshareFont, category: 'Serif' }; const result = normalizeFontshareFont(font); expect(result.category).toBe('serif'); }); it('maps Fontshare Display to display category', () => { const font = { ...mockFontshareFont, category: 'Display' }; const result = normalizeFontshareFont(font); expect(result.category).toBe('display'); }); it('maps Fontshare Script to handwriting category', () => { const font = { ...mockFontshareFont, category: 'Script' }; const result = normalizeFontshareFont(font); expect(result.category).toBe('handwriting'); }); it('maps Fontshare Mono to monospace category', () => { const font = { ...mockFontshareFont, category: 'Mono' }; const result = normalizeFontshareFont(font); expect(result.category).toBe('monospace'); }); it('maps style URLs correctly', () => { const result = normalizeFontshareFont(mockFontshareFont); expect(result.styles.regular).toBe('//cdn.fontshare.com/wf/satoshi.woff2'); expect(result.styles.bold).toBe('//cdn.fontshare.com/wf/satoshi-bold.woff2'); expect(result.styles.italic).toBe('//cdn.fontshare.com/wf/satoshi-italic.woff2'); expect(result.styles.boldItalic).toBe( '//cdn.fontshare.com/wf/satoshi-bolditalic.woff2', ); }); it('handles variable fonts', () => { const variableFont: FontshareFont = { ...mockFontshareFont, axes: [ { name: 'wght', property: 'wght', range_default: 400, range_left: 300, range_right: 900, }, ], styles: [ { id: 'var-style', default: true, file: '//cdn.fontshare.com/wf/satoshi-variable.woff2', is_italic: false, is_variable: true, properties: {}, weight: { label: 'Variable', name: 'Variable', native_name: null, number: 0, weight: 0, }, }, ], }; const result = normalizeFontshareFont(variableFont); expect(result.features.isVariable).toBe(true); expect(result.features.axes).toHaveLength(1); expect(result.features.axes?.[0].name).toBe('wght'); }); it('extracts font tags', () => { const result = normalizeFontshareFont(mockFontshareFont); expect(result.features.tags).toContain('Branding'); expect(result.features.tags).toContain('Logos'); expect(result.features.tags).toHaveLength(2); }); it('includes popularity from views', () => { const result = normalizeFontshareFont(mockFontshareFont); expect(result.metadata.popularity).toBe(10000); }); it('includes metadata', () => { const result = normalizeFontshareFont(mockFontshareFont); expect(result.metadata.cachedAt).toBeDefined(); expect(result.metadata.version).toBe('1.0'); expect(result.metadata.lastModified).toBe('2021-03-12T20:49:05Z'); }); it('handles missing subsets gracefully', () => { const font = { ...mockFontshareFont, script: 'invalid-script', }; const result = normalizeFontshareFont(font); expect(result.subsets).toEqual([]); }); it('handles empty tags', () => { const font = { ...mockFontshareFont, font_tags: [], }; const result = normalizeFontshareFont(font); expect(result.features.tags).toBeUndefined(); }); it('handles empty axes', () => { const font = { ...mockFontshareFont, axes: [], }; const result = normalizeFontshareFont(font); expect(result.features.isVariable).toBe(false); expect(result.features.axes).toBeUndefined(); }); }); describe('normalizeGoogleFonts', () => { it('normalizes array of Google Fonts', () => { const fonts: GoogleFontItem[] = [ { family: 'Roboto', category: 'sans-serif', variants: ['regular'], subsets: ['latin'], files: { regular: 'url' }, version: 'v1', lastModified: '2022-01-01', menu: 'https://fonts.googleapis.com/css2?family=Roboto', }, { family: 'Open Sans', category: 'sans-serif', variants: ['regular'], subsets: ['latin'], files: { regular: 'url' }, version: 'v1', lastModified: '2022-01-01', menu: 'https://fonts.googleapis.com/css2?family=Open+Sans', }, ]; const result = normalizeGoogleFonts(fonts); expect(result).toHaveLength(2); expect(result[0].name).toBe('Roboto'); expect(result[1].name).toBe('Open Sans'); }); it('returns empty array for empty input', () => { const result = normalizeGoogleFonts([]); expect(result).toEqual([]); }); }); describe('normalizeFontshareFonts', () => { it('normalizes array of Fontshare fonts', () => { const fonts: FontshareFont[] = [ { ...mockMinimalFontshareFont('font1', 'Font 1'), }, { ...mockMinimalFontshareFont('font2', 'Font 2'), }, ]; const result = normalizeFontshareFonts(fonts); expect(result).toHaveLength(2); expect(result[0].name).toBe('Font 1'); expect(result[1].name).toBe('Font 2'); }); it('returns empty array for empty input', () => { const result = normalizeFontshareFonts([]); expect(result).toEqual([]); }); }); describe('edge cases', () => { it('handles Google Font with missing optional fields', () => { const font: Partial