UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

264 lines (205 loc) • 7.37 kB
import { TLShape, TLShapeId, createShapeId } from '@tldraw/tlschema' import { Editor } from '../../Editor' import { FontManager, TLFontFace } from './FontManager' // Mock the Editor class jest.mock('../../Editor') // Mock globals global.FontFace = jest.fn().mockImplementation((family, src, descriptors) => ({ family, src, ...descriptors, load: jest.fn(() => Promise.resolve()), })) Object.defineProperty(global.document, 'fonts', { value: { add: jest.fn(), [Symbol.iterator]: jest.fn(() => [].values()), }, configurable: true, }) global.queueMicrotask = jest.fn((fn) => Promise.resolve().then(fn)) describe('FontManager', () => { let editor: jest.Mocked<Editor> let fontManager: FontManager let mockAssetUrls: { [key: string]: string } const createMockFont = (overrides: Partial<TLFontFace> = {}): TLFontFace => ({ family: 'Test Font', src: { url: 'test-font.woff2' }, ...overrides, }) const createMockShape = (id: TLShapeId = createShapeId('test')): TLShape => ({ id, type: 'text', x: 0, y: 0, rotation: 0, index: 'a1' as any, parentId: 'page:page' as any, opacity: 1, isLocked: false, meta: {}, props: {}, typeName: 'shape' as const, }) beforeEach(() => { jest.clearAllMocks() mockAssetUrls = { 'test-font.woff2': 'https://example.com/fonts/test-font.woff2', } const mockShapeUtil = { getFontFaces: jest.fn(() => []), } const mockStore = { createComputedCache: jest.fn(() => ({ get: jest.fn(() => []), })), createCache: jest.fn(() => ({ get: jest.fn(() => ({ get: jest.fn(() => []) })), })), } editor = { store: mockStore, getShapeUtil: jest.fn(() => mockShapeUtil), getCurrentPageShapeIds: jest.fn(() => new Set()), getShape: jest.fn(), isDisposed: false, } as any fontManager = new FontManager(editor, mockAssetUrls) }) describe('constructor', () => { it('should initialize with editor reference', () => { expect(fontManager).toBeDefined() }) it('should initialize without assetUrls', () => { const managerWithoutUrls = new FontManager(editor) expect(managerWithoutUrls).toBeDefined() }) }) describe('getShapeFontFaces', () => { it('should return empty array when no fonts found', () => { const shape = createMockShape() const result = fontManager.getShapeFontFaces(shape) expect(result).toEqual([]) }) it('should accept shape ID as parameter', () => { const shapeId = createShapeId('test') const result = fontManager.getShapeFontFaces(shapeId) expect(result).toEqual([]) }) }) describe('trackFontsForShape', () => { it('should track fonts for shape without throwing', () => { const shape = createMockShape() expect(() => fontManager.trackFontsForShape(shape)).not.toThrow() }) it('should track fonts for shape ID without throwing', () => { const shapeId = createShapeId('test') expect(() => fontManager.trackFontsForShape(shapeId)).not.toThrow() }) }) describe('loadRequiredFontsForCurrentPage', () => { it('should complete without error when no fonts needed', async () => { await expect(fontManager.loadRequiredFontsForCurrentPage()).resolves.toBeUndefined() }) it('should respect font limit', async () => { const shapeIds = Array.from({ length: 5 }, (_, i) => createShapeId(`test${i}`)) const shapes = shapeIds.map(createMockShape) editor.getCurrentPageShapeIds.mockReturnValue(new Set(shapeIds)) editor.getShape.mockImplementation((id) => shapes.find((s) => s.id === id)) await expect(fontManager.loadRequiredFontsForCurrentPage(3)).resolves.toBeUndefined() }) }) describe('ensureFontIsLoaded', () => { it('should create and load font face', async () => { const font = createMockFont() await fontManager.ensureFontIsLoaded(font) expect(global.FontFace).toHaveBeenCalledWith( font.family, expect.stringContaining('url('), expect.any(Object) ) }) it('should handle font loading errors gracefully', async () => { const font = createMockFont() const error = new Error('Font load failed') ;(global.FontFace as jest.Mock).mockReturnValue({ family: font.family, load: jest.fn(() => Promise.reject(error)), }) const consoleSpy = jest.spyOn(console, 'error').mockImplementation() await fontManager.ensureFontIsLoaded(font) expect(consoleSpy).toHaveBeenCalledWith(error) consoleSpy.mockRestore() }) it('should return same promise for concurrent requests', async () => { const font = createMockFont() const promise1 = fontManager.ensureFontIsLoaded(font) const promise2 = fontManager.ensureFontIsLoaded(font) expect(promise1).toBe(promise2) await Promise.all([promise1, promise2]) }) }) describe('requestFonts', () => { it('should queue fonts for loading', () => { const fonts = [createMockFont({ family: 'Font1' }), createMockFont({ family: 'Font2' })] fontManager.requestFonts(fonts) expect(queueMicrotask).toHaveBeenCalled() }) it('should deduplicate font requests', () => { const font = createMockFont() fontManager.requestFonts([font]) fontManager.requestFonts([font]) expect(queueMicrotask).toHaveBeenCalledTimes(1) }) it('should handle editor disposal during async loading', () => { const fonts = [createMockFont()] editor.isDisposed = true fontManager.requestFonts(fonts) const callback = (queueMicrotask as jest.Mock).mock.calls[0][0] expect(() => callback()).not.toThrow() }) }) describe('toEmbeddedCssDeclaration', () => { it('should generate font CSS without data conversion (simplified test)', async () => { const font = createMockFont() // Mock the actual method implementation to avoid FileHelpers dependency const mockCssDeclaration = `@font-face { font-family: "${font.family}"; src: url("mock-data-url"); }` jest.spyOn(fontManager, 'toEmbeddedCssDeclaration').mockResolvedValue(mockCssDeclaration) const result = await fontManager.toEmbeddedCssDeclaration(font) expect(result).toContain(`font-family: "${font.family}";`) expect(result).toContain('src:') expect(result).toContain('@font-face {') expect(result).toContain('}') }) it('should call toEmbeddedCssDeclaration method', async () => { const font = createMockFont() // Simple spy to verify the method is called const spy = jest.spyOn(fontManager, 'toEmbeddedCssDeclaration').mockResolvedValue('mock-css') await fontManager.toEmbeddedCssDeclaration(font) expect(spy).toHaveBeenCalledWith(font) spy.mockRestore() }) }) describe('error handling and edge cases', () => { it('should handle empty getCurrentPageShapeIds', () => { editor.getCurrentPageShapeIds.mockReturnValue(new Set()) expect(() => fontManager.loadRequiredFontsForCurrentPage()).not.toThrow() }) it('should handle null shape from getShape', async () => { const shapeId = createShapeId('test') editor.getCurrentPageShapeIds.mockReturnValue(new Set([shapeId])) editor.getShape.mockReturnValue(undefined) await expect(fontManager.loadRequiredFontsForCurrentPage()).rejects.toThrow() }) it('should handle fonts with minimal properties', async () => { const minimalFont: TLFontFace = { family: 'Minimal Font', src: { url: 'minimal.woff2' }, } await expect(fontManager.ensureFontIsLoaded(minimalFont)).resolves.toBeUndefined() }) }) })