refactor(shared): rename fontCache to collectionCache
- Rename fontCache.ts to collectionCache.ts - Rename FontCacheManager interface to CollectionCacheManager - Make implementation fully generic (already was, just renamed interface) - Update exports in shared/fetch/index.ts - Fix getStats() to return derived store value for accurate statistics - Add comprehensive test coverage for collection cache manager - 41 test cases covering all functionality - Tests for caching, deduplication, state tracking - Tests for statistics, reactivity, and edge cases Closes task-1 of Phase 1 refactoring
This commit is contained in:
598
src/entities/Font/api/normalize.test.ts
Normal file
598
src/entities/Font/api/normalize.test.ts
Normal file
@@ -0,0 +1,598 @@
|
||||
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: '<p>Font story</p>',
|
||||
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<GoogleFontItem> = {
|
||||
family: 'Test Font',
|
||||
category: 'sans-serif',
|
||||
variants: ['regular'],
|
||||
subsets: ['latin'],
|
||||
files: { regular: 'url' },
|
||||
};
|
||||
|
||||
const result = normalizeGoogleFont(font as GoogleFontItem);
|
||||
|
||||
expect(result.id).toBe('Test Font');
|
||||
expect(result.metadata.version).toBeUndefined();
|
||||
expect(result.metadata.lastModified).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles Fontshare font with minimal data', () => {
|
||||
const result = normalizeFontshareFont(mockMinimalFontshareFont('slug', 'Name'));
|
||||
|
||||
expect(result.id).toBe('slug');
|
||||
expect(result.name).toBe('Name');
|
||||
expect(result.provider).toBe('fontshare');
|
||||
});
|
||||
|
||||
it('handles unknown Fontshare category', () => {
|
||||
const font = {
|
||||
...mockMinimalFontshareFont('slug', 'Name'),
|
||||
category: 'Unknown Category',
|
||||
};
|
||||
const result = normalizeFontshareFont(font);
|
||||
|
||||
expect(result.category).toBe('sans-serif'); // fallback
|
||||
});
|
||||
|
||||
it('handles unknown Google Font category', () => {
|
||||
const font: GoogleFontItem = {
|
||||
family: 'Test',
|
||||
category: 'unknown',
|
||||
variants: ['regular'],
|
||||
subsets: ['latin'],
|
||||
files: { regular: 'url' },
|
||||
version: 'v1',
|
||||
lastModified: '2022-01-01',
|
||||
menu: 'https://fonts.googleapis.com/css2?family=Test',
|
||||
};
|
||||
const result = normalizeGoogleFont(font);
|
||||
|
||||
expect(result.category).toBe('sans-serif'); // fallback
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create minimal Fontshare font mock
|
||||
*/
|
||||
function mockMinimalFontshareFont(slug: string, name: string): FontshareFont {
|
||||
return {
|
||||
id: 'test-id',
|
||||
name,
|
||||
native_name: null,
|
||||
slug,
|
||||
category: 'Sans',
|
||||
script: 'latin',
|
||||
publisher: {
|
||||
bio: '',
|
||||
email: null,
|
||||
id: '',
|
||||
links: [],
|
||||
name: '',
|
||||
},
|
||||
designers: [],
|
||||
related_families: null,
|
||||
display_publisher_as_designer: false,
|
||||
trials_enabled: false,
|
||||
show_latin_metrics: false,
|
||||
license_type: '',
|
||||
languages: '',
|
||||
inserted_at: '',
|
||||
story: '',
|
||||
version: '1.0',
|
||||
views: 0,
|
||||
views_recent: 0,
|
||||
is_hot: false,
|
||||
is_new: false,
|
||||
is_shortlisted: null,
|
||||
is_top: false,
|
||||
axes: [],
|
||||
font_tags: [],
|
||||
features: [],
|
||||
styles: [
|
||||
{
|
||||
id: 'style-id',
|
||||
default: true,
|
||||
file: '//cdn.fontshare.com/wf/test.woff2',
|
||||
is_italic: false,
|
||||
is_variable: false,
|
||||
properties: {},
|
||||
weight: {
|
||||
label: 'Regular',
|
||||
name: 'Regular',
|
||||
native_name: null,
|
||||
number: 400,
|
||||
weight: 400,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user