import { describe, expect, it, } from 'vitest'; import type { UnifiedFont } from '../../model/types'; import { getFontUrl } from './getFontUrl'; /** * Helper function to create a minimal UnifiedFont mock for testing */ 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('getFontUrl', () => { describe('basic logic', () => { it('returns URL for exact weight match in variants', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', '700': 'https://example.com/font-700.woff2', }, }, }); const result = getFontUrl(font, 400); expect(result).toBe('https://example.com/font-400.woff2'); }); it('returns URL for weight 700', () => { const font = createMockFont({ styles: { variants: { '700': 'https://example.com/font-700.woff2', }, }, }); const result = getFontUrl(font, 700); expect(result).toBe('https://example.com/font-700.woff2'); }); it('returns URL for weight 100 (lightest)', () => { const font = createMockFont({ styles: { variants: { '100': 'https://example.com/font-100.woff2', }, }, }); const result = getFontUrl(font, 100); expect(result).toBe('https://example.com/font-100.woff2'); }); it('returns URL for weight 900 (boldest)', () => { const font = createMockFont({ styles: { variants: { '900': 'https://example.com/font-900.woff2', }, }, }); const result = getFontUrl(font, 900); expect(result).toBe('https://example.com/font-900.woff2'); }); it('returns URL for variable font (backend maps weight to VF URL)', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-variable.woff2', '700': 'https://example.com/font-variable.woff2', }, }, }); const result400 = getFontUrl(font, 400); const result700 = getFontUrl(font, 700); expect(result400).toBe('https://example.com/font-variable.woff2'); expect(result700).toBe('https://example.com/font-variable.woff2'); }); }); describe('fallback logic', () => { it('falls back to regular when exact weight not found', () => { const font = createMockFont({ styles: { regular: 'https://example.com/font-regular.woff2', variants: { '400': 'https://example.com/font-400.woff2', }, }, }); const result = getFontUrl(font, 700); expect(result).toBe('https://example.com/font-regular.woff2'); }); it('falls back to variant 400 when exact weight and regular not found', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); const result = getFontUrl(font, 700); expect(result).toBe('https://example.com/font-400.woff2'); }); it('falls back to variant regular when exact weight, regular, and 400 not found', () => { const font = createMockFont({ styles: { variants: { '700': 'https://example.com/font-700.woff2', 'regular': 'https://example.com/font-regular.woff2', }, }, }); const result = getFontUrl(font, 400); expect(result).toBe('https://example.com/font-regular.woff2'); }); it('prefers regular over variants.400 for fallback', () => { const font = createMockFont({ styles: { regular: 'https://example.com/font-regular.woff2', variants: { '400': 'https://example.com/font-400.woff2', }, }, }); const result = getFontUrl(font, 700); expect(result).toBe('https://example.com/font-regular.woff2'); }); it('returns undefined when no fallback options available', () => { const font = createMockFont({ styles: { variants: { '700': 'https://example.com/font-700.woff2', }, }, }); const result = getFontUrl(font, 400); expect(result).toBeUndefined(); }); it('returns undefined for font with empty styles', () => { const font = createMockFont({ styles: {}, }); const result = getFontUrl(font, 400); expect(result).toBeUndefined(); }); it('throws error for font with undefined styles (invalid font data)', () => { const font = createMockFont({ styles: undefined as any, }); expect(() => getFontUrl(font, 400)).toThrow(); }); }); describe('edge cases', () => { it('handles font with only regular URL (legacy format)', () => { const font = createMockFont({ styles: { regular: 'https://example.com/font-regular.woff2', }, }); const result = getFontUrl(font, 700); expect(result).toBe('https://example.com/font-regular.woff2'); }); it('handles font with only variants object', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', '700': 'https://example.com/font-700.woff2', }, }, }); const result400 = getFontUrl(font, 400); const result700 = getFontUrl(font, 700); expect(result400).toBe('https://example.com/font-400.woff2'); expect(result700).toBe('https://example.com/font-700.woff2'); }); it('handles font with variants but no requested weight', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); const result = getFontUrl(font, 700); expect(result).toBe('https://example.com/font-400.woff2'); }); it('handles Google Fonts style with legacy URLs', () => { const font = createMockFont({ styles: { regular: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2', bold: 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu72xWUlvAx05IsDqlA.woff2', }, }); const result = getFontUrl(font, 700); expect(result).toBe('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2'); }); it('handles Fontshare fonts with multiple weights', () => { const font = createMockFont({ styles: { variants: { '100': 'https://cdn.fontshare.com/wf/font-100.woff2', '200': 'https://cdn.fontshare.com/wf/font-200.woff2', '300': 'https://cdn.fontshare.com/wf/font-300.woff2', '400': 'https://cdn.fontshare.com/wf/font-400.woff2', '500': 'https://cdn.fontshare.com/wf/font-500.woff2', '600': 'https://cdn.fontshare.com/wf/font-600.woff2', '700': 'https://cdn.fontshare.com/wf/font-700.woff2', '800': 'https://cdn.fontshare.com/wf/font-800.woff2', '900': 'https://cdn.fontshare.com/wf/font-900.woff2', }, }, }); // Test all valid weights for (const weight of [100, 200, 300, 400, 500, 600, 700, 800, 900]) { const result = getFontUrl(font, weight); expect(result).toBe(`https://cdn.fontshare.com/wf/font-${weight}.woff2`); } }); it('handles font with partial weight coverage', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-regular.woff2', '700': 'https://example.com/font-bold.woff2', }, }, }); const result400 = getFontUrl(font, 400); const result700 = getFontUrl(font, 700); const result500 = getFontUrl(font, 500); expect(result400).toBe('https://example.com/font-regular.woff2'); expect(result700).toBe('https://example.com/font-bold.woff2'); expect(result500).toBe('https://example.com/font-regular.woff2'); // Fallback }); it('handles font with variants.regular as fallback', () => { const font = createMockFont({ styles: { variants: { '700': 'https://example.com/font-bold.woff2', 'regular': 'https://example.com/font-regular.woff2', }, }, }); const result = getFontUrl(font, 400); expect(result).toBe('https://example.com/font-regular.woff2'); }); it('handles empty variants object', () => { const font = createMockFont({ styles: { variants: {}, }, }); const result = getFontUrl(font, 400); expect(result).toBeUndefined(); }); it('returns undefined when variant URL is null and no fallback available', () => { const font = createMockFont({ styles: { variants: { '400': null as any, '700': 'https://example.com/font-bold.woff2', }, }, }); const result = getFontUrl(font, 400); // null is falsy, so it falls back to regular, 400, and then regular variant // All are undefined, so returns undefined expect(result).toBeUndefined(); }); }); describe('boundary tests', () => { it('handles lowest valid weight (100)', () => { const font = createMockFont({ styles: { variants: { '100': 'https://example.com/font-100.woff2', }, }, }); const result = getFontUrl(font, 100); expect(result).toBe('https://example.com/font-100.woff2'); }); it('handles highest valid weight (900)', () => { const font = createMockFont({ styles: { variants: { '900': 'https://example.com/font-900.woff2', }, }, }); const result = getFontUrl(font, 900); expect(result).toBe('https://example.com/font-900.woff2'); }); it('handles middle weight (500)', () => { const font = createMockFont({ styles: { variants: { '500': 'https://example.com/font-500.woff2', }, }, }); const result = getFontUrl(font, 500); expect(result).toBe('https://example.com/font-500.woff2'); }); }); describe('invalid weights', () => { it('throws error for weight below 100', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); expect(() => getFontUrl(font, 99)).toThrow('Invalid weight: 99'); }); it('throws error for weight above 900', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); expect(() => getFontUrl(font, 901)).toThrow('Invalid weight: 901'); }); it('throws error for weight 0', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); expect(() => getFontUrl(font, 0)).toThrow('Invalid weight: 0'); }); it('throws error for negative weight', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); expect(() => getFontUrl(font, -100)).toThrow('Invalid weight: -100'); }); it('throws error for non-numeric weight', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); // @ts-ignore - Testing invalid input type expect(() => getFontUrl(font, '400' as any)).toThrow('Invalid weight: 400'); }); it('throws error for decimal weight', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); expect(() => getFontUrl(font, 450.5)).toThrow('Invalid weight: 450.5'); }); it('throws error for weight with step of 50 (not supported)', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); expect(() => getFontUrl(font, 450)).toThrow('Invalid weight: 450'); }); it('throws error for weight with step of 10 (not supported)', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); expect(() => getFontUrl(font, 410)).toThrow('Invalid weight: 410'); }); it('throws error for NaN weight', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); expect(() => getFontUrl(font, NaN)).toThrow('Invalid weight: NaN'); }); it('throws error for Infinity weight', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); expect(() => getFontUrl(font, Infinity)).toThrow('Invalid weight: Infinity'); }); it('throws descriptive error message', () => { const font = createMockFont({ styles: { variants: { '400': 'https://example.com/font-400.woff2', }, }, }); try { getFontUrl(font, 999); expect.fail('Expected function to throw'); } catch (error) { expect(error).toBeInstanceOf(Error); expect((error as Error).message).toBe('Invalid weight: 999'); } }); }); describe('provider-specific tests', () => { it('handles Google Fonts with variable fonts', () => { const font = createMockFont({ provider: 'google', styles: { variants: { '400': 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2', '700': 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2', }, }, }); const result400 = getFontUrl(font, 400); const result700 = getFontUrl(font, 700); // Variable fonts return the same URL for all weights expect(result400).toBe(result700); }); it('handles Fontshare fonts with static weights', () => { const font = createMockFont({ provider: 'fontshare', styles: { variants: { '400': 'https://cdn.fontshare.com/wf/satoshi-regular.woff2', '700': 'https://cdn.fontshare.com/wf/satoshi-bold.woff2', }, }, }); const result400 = getFontUrl(font, 400); const result700 = getFontUrl(font, 700); expect(result400).toBe('https://cdn.fontshare.com/wf/satoshi-regular.woff2'); expect(result700).toBe('https://cdn.fontshare.com/wf/satoshi-bold.woff2'); expect(result400).not.toBe(result700); }); }); describe('all valid weights test', () => { it('handles all valid weight values', () => { const validWeights = [100, 200, 300, 400, 500, 600, 700, 800, 900]; validWeights.forEach(weight => { const font = createMockFont({ styles: { variants: { [weight.toString()]: `https://example.com/font-${weight}.woff2`, }, }, }); const result = getFontUrl(font, weight); expect(result).toBe(`https://example.com/font-${weight}.woff2`); }); }); }); });