593 lines
19 KiB
TypeScript
593 lines
19 KiB
TypeScript
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> = {},
|
|
): 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`);
|
|
});
|
|
});
|
|
});
|
|
});
|