import { describe, expect, it } from 'vitest'; import { CURRENT_FONT_VERSION, createFontAsset, detectFontFormat, familyNameFromFileName, type FontAxis, fontFamilyStack, isVariableFont, parseFontAxes, variableFontDescriptors, } from './font-asset'; const bytes = (n: number): ArrayBuffer => new Uint8Array(n).fill(1).buffer; /** * Build a minimal SFNT with an `fvar` table carrying the given axes, so the * parser is exercised against real byte layout without committing a font binary. * Layout: 12-byte sfnt header, one 16-byte table record ('fvar'), then the fvar * table (16-byte header + 20 bytes per axis). Mirrors the offsets parseFontAxes * reads — verified against a real Fixel variable file during development. */ function buildVariableSfnt(axes: Array<[string, number, number, number]>): ArrayBuffer { const axisSize = 20; const fvarHeaderLen = 16; const fvarLen = fvarHeaderLen + axes.length * axisSize; const tableOffset = 12 + 16; // sfnt header + one table record const buf = new ArrayBuffer(tableOffset + fvarLen); const v = new DataView(buf); v.setUint32(0, 0x00010000); // sfntVersion (TrueType) v.setUint16(4, 1); // numTables for (let i = 0; i < 4; i++) v.setUint8(12 + i, 'fvar'.charCodeAt(i)); v.setUint32(12 + 8, tableOffset); // table record: offset v.setUint32(12 + 12, fvarLen); // table record: length v.setUint16(tableOffset, 1); // fvar major version v.setUint16(tableOffset + 4, fvarHeaderLen); // axesArrayOffset v.setUint16(tableOffset + 8, axes.length); // axisCount v.setUint16(tableOffset + 10, axisSize); // axisSize const base = tableOffset + fvarHeaderLen; axes.forEach(([tag, min, def, max], i) => { const a = base + i * axisSize; for (let j = 0; j < 4; j++) v.setUint8(a + j, tag.charCodeAt(j)); v.setInt32(a + 4, min * 65536); v.setInt32(a + 8, def * 65536); v.setInt32(a + 12, max * 65536); }); return buf; } describe('detectFontFormat', () => { it('maps supported extensions, case-insensitively', () => { expect(detectFontFormat('Inter.woff2')).toBe('woff2'); expect(detectFontFormat('Inter.WOFF')).toBe('woff'); expect(detectFontFormat('Inter.ttf')).toBe('ttf'); expect(detectFontFormat('Inter.OTF')).toBe('otf'); }); it('reads the last extension and rejects unsupported / extensionless names', () => { expect(detectFontFormat('My.Font.woff2')).toBe('woff2'); expect(detectFontFormat('Inter.eot')).toBeNull(); expect(detectFontFormat('Inter')).toBeNull(); expect(detectFontFormat('')).toBeNull(); }); }); describe('familyNameFromFileName', () => { it('drops the extension and normalizes separators', () => { expect(familyNameFromFileName('Inter-Regular.ttf')).toBe('Inter Regular'); expect(familyNameFromFileName('my_cool_font.woff2')).toBe('my cool font'); expect(familyNameFromFileName('Spaced Out.otf')).toBe('Spaced Out'); }); it('falls back when the name reduces to nothing', () => { expect(familyNameFromFileName('.woff2')).toBe('Custom font'); }); }); describe('createFontAsset', () => { it('stamps version/timestamps/size and defaults source to file', () => { const now = new Date('2026-06-15T12:00:00.000Z'); const asset = createFontAsset({ family: 'My Font', data: bytes(2048), format: 'woff2', fileName: 'My-Font.woff2', now, id: 7, }); expect(asset).toMatchObject({ id: 7, version: CURRENT_FONT_VERSION, family: 'My Font', format: 'woff2', fileName: 'My-Font.woff2', source: 'file', size: 2048, created: now.toISOString(), modified: now.toISOString(), }); }); it('carries an explicit source', () => { expect( createFontAsset({ family: 'G', data: bytes(1), format: 'woff2', fileName: 'g.woff2', source: 'google', }).source, ).toBe('google'); }); }); describe('fontFamilyStack', () => { it('quotes the family and appends a generic fallback', () => { expect(fontFamilyStack('My Font')).toBe('"My Font", sans-serif'); }); }); describe('parseFontAxes', () => { it('reads variation axes from an SFNT fvar table (ttf/otf)', () => { const sfnt = buildVariableSfnt([ ['wght', 100, 400, 900], ['wdth', 75, 100, 100], ]); expect(parseFontAxes(sfnt, 'ttf')).toEqual([ { tag: 'wght', min: 100, default: 400, max: 900 }, { tag: 'wdth', min: 75, default: 100, max: 100 }, ]); expect(parseFontAxes(sfnt, 'otf')).toHaveLength(2); }); it('returns [] for a static font (no fvar table)', () => { // A valid sfnt header claiming zero tables — no fvar to find. const buf = new ArrayBuffer(12); new DataView(buf).setUint32(0, 0x00010000); expect(parseFontAxes(buf, 'ttf')).toEqual([]); }); it('treats woff/woff2 as static — their tables are compressed, not parsed', () => { const sfnt = buildVariableSfnt([['wght', 100, 400, 900]]); expect(parseFontAxes(sfnt, 'woff2')).toEqual([]); expect(parseFontAxes(sfnt, 'woff')).toEqual([]); }); it('degrades to [] on a malformed file rather than throwing', () => { expect(parseFontAxes(new Uint8Array([1, 2, 3]).buffer, 'ttf')).toEqual([]); }); }); describe('variable-font helpers', () => { const wght: FontAxis = { tag: 'wght', min: 100, default: 400, max: 900 }; const wdth: FontAxis = { tag: 'wdth', min: 75, default: 100, max: 100 }; it('isVariableFont reflects whether axes are present', () => { expect(isVariableFont([wght])).toBe(true); expect(isVariableFont([])).toBe(false); expect(isVariableFont(undefined)).toBe(false); }); it('maps wght→weight range and wdth→stretch range; ignores other axes', () => { expect(variableFontDescriptors([wght, wdth])).toEqual({ weight: '100 900', stretch: '75% 100%', }); expect(variableFontDescriptors([{ tag: 'opsz', min: 8, default: 14, max: 144 }])).toEqual({}); expect(variableFontDescriptors(undefined)).toEqual({}); }); });