* fix(text): request script fallbacks for substituted text When a text's font could not be loaded and the default family substituted for it, font readiness returned before checking glyph coverage. That check is what requests CJK and Arabic fallbacks, so text such as Chinese in an unavailable PingFang SC drew missing glyphs unless another layer happened to request the fallback first. Substituted text now observes glyph coverage too. It waits while a fallback loads and stays visible when none is available. * fix(fonts): explain installed fonts with unsupported outlines On macOS 15 and later PingFang ships only `hvgl` outlines, which neither font-kit nor CanvasKit can read. The desktop loader spent over a second parsing the collection per style, and the font banner showed PingFang as substituted with no explanation. The loader now reads the family's table directories first and returns a structured unsupported-format error. The font manager records it per face, document font status exposes it as `reason`, and the banner shows it inline with the full explanation in a tooltip. The resolver reports progress after each failed candidate so the banner updates before web font lookups finish. * fix(fonts): keep the unsupported-format reason after failed retries A later host attempt that returns no font no longer clears the reason; only a loaded face does.
68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
import { afterEach, describe, expect, test, vi } from 'bun:test'
|
|
|
|
import { fontManager } from '@open-pencil/core/text'
|
|
|
|
import { clearTauriMocks, mockTauriIPC } from '#tests/helpers/tauri/mocks'
|
|
|
|
afterEach(async () => {
|
|
await clearTauriMocks()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('Tauri font helpers', () => {
|
|
test('lists system font families through mocked Tauri IPC', async () => {
|
|
await mockTauriIPC((cmd) => {
|
|
expect(cmd).toBe('list_system_fonts')
|
|
return [{ family: 'System UI', styles: ['Regular', 'Bold'] }]
|
|
})
|
|
|
|
vi.spyOn(fontManager, 'listFamilyOptions').mockResolvedValue([])
|
|
const { listFamilies, listFonts } = await import('@/app/editor/fonts')
|
|
|
|
await expect(listFamilies()).resolves.toEqual([{ family: 'System UI', source: 'local' }])
|
|
await expect(listFonts()).resolves.toEqual([
|
|
{ family: 'System UI', styles: ['Regular', 'Bold'] }
|
|
])
|
|
})
|
|
|
|
test('loads system font bytes and registers the face', async () => {
|
|
await mockTauriIPC((cmd, args) => {
|
|
expect(cmd).toBe('load_system_font')
|
|
expect(args).toEqual({ family: 'System UI', style: 'Bold Italic' })
|
|
return new Uint8Array([1, 2, 3, 4]).buffer
|
|
})
|
|
|
|
const { loadFont } = await import('@/app/editor/fonts')
|
|
const buffer = await loadFont('System UI', 'Bold Italic')
|
|
|
|
expect([...new Uint8Array(buffer ?? new ArrayBuffer(0))]).toEqual([1, 2, 3, 4])
|
|
expect(fontManager.isLoaded('System UI', 'Bold Italic')).toBe(true)
|
|
})
|
|
|
|
test('records installed faces whose outlines cannot be drawn', async () => {
|
|
await mockTauriIPC((cmd) => {
|
|
expect(cmd).toBe('load_system_font')
|
|
throw { code: 'unsupported-format', message: 'Font outlines are in an unsupported format' }
|
|
})
|
|
await import('@/app/editor/fonts')
|
|
|
|
await expect(fontManager.loadLocalFont('Undrawable Sans', 'Regular')).resolves.toBeNull()
|
|
expect(fontManager.unavailableReason('Undrawable Sans', 'Regular')).toBe('unsupported-format')
|
|
expect(fontManager.unavailableReason('Undrawable Sans', 'Bold')).toBeNull()
|
|
})
|
|
|
|
test('falls back to font manager loading when the system font command fails', async () => {
|
|
await mockTauriIPC((cmd) => {
|
|
expect(cmd).toBe('load_system_font')
|
|
throw new Error('missing system font')
|
|
})
|
|
const fallback = new Uint8Array([9, 8, 7]).buffer
|
|
const loadFontSpy = vi.spyOn(fontManager, 'loadFont').mockResolvedValue(fallback)
|
|
|
|
const { loadFont } = await import('@/app/editor/fonts')
|
|
|
|
await expect(loadFont('Missing Family', 'Regular')).resolves.toBe(fallback)
|
|
expect(loadFontSpy).toHaveBeenCalledWith('Missing Family', 'Regular', '', undefined)
|
|
})
|
|
})
|