/** * Importing a typeface: the same two ways a picture arrives – picked from what the server already * holds, or uploaded from a dropped file – with the same content-addressed reuse. */ import { describe, expect, it, vi } from 'vitest'; import type { FontAsset } from '../../src/core/model/Asset'; import { createEmptyProject } from '../../src/core/model/Project'; import type { FontStore } from '../../src/core/store/FontStore'; import { importFonts, importStoredFont, isFontFileName } from '../../src/editor/fontImport'; import { EditorState } from '../../src/editor/state'; import { importNotice } from '../../src/editor/assetImport'; const FILE = `${'a'.repeat(64)}.ttf`; const font = { file: FILE, name: '', bytes: 380_660 }; function setup(): EditorState { const state = new EditorState(); state.loadProject(createEmptyProject('p', 'P'), 1); return state; } describe('isFontFileName', () => { it('goes by the extension, because a dropped font often has no MIME type at all', () => { expect(isFontFileName('DejaVuSerif.ttf')).toBe(true); expect(isFontFileName('Inter.WOFF2')).toBe(true); expect(isFontFileName('cover.png')).toBe(false); expect(isFontFileName('ttf')).toBe(false); }); }); describe('importStoredFont', () => { const fonts = { upload: vi.fn(), list: vi.fn(), delete: vi.fn() } as unknown as FontStore; it('makes an asset out of a typeface the server already holds', () => { const state = setup(); const outcome = importStoredFont(font, { state, fonts }); const asset = state.project.assets.find((a): a is FontAsset => a.type === 'font'); expect(asset).toMatchObject({ file: FILE, name: 'Font' }); expect(outcome).toMatchObject({ assetId: asset?.id, error: null, reused: false }); expect(state.selection).toEqual({ kind: 'asset', assetId: asset?.id }); expect(state.notice).toBe('Added the font to the project.'); state.undo(); expect(state.project.assets.filter((a) => a.type === 'font')).toEqual([]); }); it('names the asset after the file it was uploaded as, not "Font"', () => { const state = setup(); importStoredFont({ ...font, name: 'DejaVuSerif.ttf' }, { state, fonts }); expect(state.project.assets.find((a): a is FontAsset => a.type === 'font')).toMatchObject({ file: FILE, name: 'DejaVuSerif', }); }); it('reuses the asset that already names the file instead of making a second one', () => { const state = setup(); const first = importStoredFont(font, { state, fonts }); const again = importStoredFont(font, { state, fonts }); expect(again.assetId).toBe(first.assetId); expect(again.reused).toBe(true); expect(state.project.assets.filter((a) => a.type === 'font')).toHaveLength(1); expect(state.notice).toBe('That font is already in the project.'); }); }); describe('importFonts', () => { it('uploads the file untouched and names the asset after it', async () => { const state = setup(); const upload = vi.fn(() => Promise.resolve(font)); const fonts = { upload, list: vi.fn(), delete: vi.fn() } as unknown as FontStore; const file = new File([new Uint8Array(8)], 'DejaVuSerif.ttf', { type: 'font/ttf' }); const [outcome] = await importFonts([file], { state, fonts }); // The blob goes up as it came in: a typeface cannot be re-encoded smaller on the way. // The name travels with the bytes: it is all the server can caption the hash with. expect(upload).toHaveBeenCalledWith(file, 'font/ttf', 'DejaVuSerif.ttf'); expect(outcome).toMatchObject({ error: null, reused: false }); expect(state.project.assets.find((a): a is FontAsset => a.type === 'font')).toMatchObject({ file: FILE, name: 'DejaVuSerif', }); expect(state.notice).toBe('Imported 1 font.'); }); it('reports a file that is not a font without asking the server about it', async () => { const state = setup(); const upload = vi.fn(() => Promise.resolve(font)); const fonts = { upload, list: vi.fn(), delete: vi.fn() } as unknown as FontStore; const [outcome] = await importFonts([new File([new Uint8Array(8)], 'cover.png')], { state, fonts, }); expect(upload).not.toHaveBeenCalled(); expect(outcome?.assetId).toBeNull(); expect(state.notice).toContain('Could not import "cover.png"'); }); }); describe('importNotice', () => { it('says what was imported, so fonts do not report as images', () => { const done = { name: 'a.ttf', assetId: 'id', error: null, reused: false }; expect(importNotice([done], 'font')).toBe('Imported 1 font.'); expect(importNotice([done, { ...done, name: 'b.ttf' }], 'font')).toBe('Imported 2 fonts.'); expect(importNotice([{ ...done, reused: true }], 'font')).toBe('That font is already in the project.'); }); });