feat(getFontUrl): create a helper function to choose font url
This commit is contained in:
592
src/entities/Font/lib/getFontUrl/getFontUrl.test.ts
Normal file
592
src/entities/Font/lib/getFontUrl/getFontUrl.test.ts
Normal file
@@ -0,0 +1,592 @@
|
||||
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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
29
src/entities/Font/lib/getFontUrl/getFontUrl.ts
Normal file
29
src/entities/Font/lib/getFontUrl/getFontUrl.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type {
|
||||
FontWeight,
|
||||
UnifiedFont,
|
||||
} from '../../model';
|
||||
|
||||
const SIZES = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
|
||||
/**
|
||||
* Constructs a URL for a font based on the provided font and weight.
|
||||
* @param font - The font object.
|
||||
* @param weight - The weight of the font.
|
||||
* @returns The URL for the font.
|
||||
*/
|
||||
export function getFontUrl(font: UnifiedFont, weight: number): string | undefined {
|
||||
if (!SIZES.includes(weight)) {
|
||||
throw new Error(`Invalid weight: ${weight}`);
|
||||
}
|
||||
|
||||
const weightKey = weight.toString() as FontWeight;
|
||||
|
||||
// 1. Try exact match (Backend now maps "100".."900" to VF URL if variable)
|
||||
if (font.styles.variants?.[weightKey]) {
|
||||
return font.styles.variants[weightKey];
|
||||
}
|
||||
|
||||
// 2. Fallbacks for Static Fonts (if exact weight missing)
|
||||
// Try 'regular' or '400' as safe defaults
|
||||
return font.styles.regular || font.styles.variants?.['400'] || font.styles.variants?.['regular'];
|
||||
}
|
||||
@@ -4,3 +4,5 @@ export {
|
||||
normalizeGoogleFont,
|
||||
normalizeGoogleFonts,
|
||||
} from './normalize/normalize';
|
||||
|
||||
export { getFontUrl } from './getFontUrl/getFontUrl';
|
||||
|
||||
Reference in New Issue
Block a user