2026-05-12 13:54:55 +00:00
|
|
|
import { test, expect } from '@playwright/test'
|
|
|
|
|
|
2026-07-17 13:19:18 +00:00
|
|
|
import type { FontManager } from '#core/text/fonts'
|
|
|
|
|
|
2026-05-12 13:54:55 +00:00
|
|
|
import { CanvasHelper } from '#tests/helpers/canvas'
|
|
|
|
|
|
2026-07-01 19:06:19 +00:00
|
|
|
test('tool-created CJK text requests fallback through app font loading', async ({ page }) => {
|
|
|
|
|
const canvas = new CanvasHelper(page)
|
|
|
|
|
await page.goto('http://localhost:1420/?test&no-chrome&no-rulers')
|
|
|
|
|
await canvas.waitForInit()
|
|
|
|
|
|
|
|
|
|
const result = await page.evaluate(async () => {
|
|
|
|
|
const store = window.openPencil?.getStore?.()
|
|
|
|
|
if (!store) throw new Error('OpenPencil store not initialized')
|
|
|
|
|
|
|
|
|
|
const { ensureGraphFonts, loadFont } = await import('/src/app/editor/fonts/index.ts')
|
|
|
|
|
await loadFont('Inter', 'Regular')
|
2026-08-10 12:48:16 +00:00
|
|
|
const fontModuleURL = performance
|
2026-07-17 13:19:18 +00:00
|
|
|
.getEntriesByType('resource')
|
|
|
|
|
.map((entry) => entry.name)
|
|
|
|
|
.find((url) => url.includes('/packages/core/src/text/fonts.ts'))
|
2026-08-10 12:48:16 +00:00
|
|
|
if (!fontModuleURL) throw new Error('Active font manager module not found')
|
|
|
|
|
const { fontManager } = (await import(/* @vite-ignore */ fontModuleURL)) as {
|
2026-07-17 13:19:18 +00:00
|
|
|
fontManager: FontManager
|
|
|
|
|
}
|
2026-07-01 19:06:19 +00:00
|
|
|
const originalEnsureFallbackPack = fontManager.ensureFallbackPack.bind(fontManager)
|
|
|
|
|
let requestedScripts: string[] = []
|
|
|
|
|
|
|
|
|
|
fontManager.ensureFallbackPack = async (scripts = ['cjk', 'arabic']) => {
|
|
|
|
|
requestedScripts = [...scripts]
|
2026-07-17 13:19:18 +00:00
|
|
|
return { cjk: ['Noto Sans CJK SC'], arabic: [] }
|
2026-07-01 19:06:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pageNode = store.graph.getNode(store.state.currentPageId)
|
|
|
|
|
if (!pageNode) throw new Error(`Page ${store.state.currentPageId} not found`)
|
|
|
|
|
const text = store.graph.createNode('TEXT', pageNode.id, {
|
|
|
|
|
name: 'Tool CJK Regression',
|
|
|
|
|
x: 80,
|
|
|
|
|
y: 80,
|
|
|
|
|
width: 300,
|
|
|
|
|
height: 60,
|
|
|
|
|
text: '你好世界',
|
|
|
|
|
fontSize: 32,
|
|
|
|
|
fontFamily: 'Inter',
|
|
|
|
|
textPicture: new Uint8Array([1, 2, 3]),
|
|
|
|
|
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, visible: true, opacity: 1 }]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const changed = await ensureGraphFonts(store.graph, [text.id])
|
|
|
|
|
return {
|
|
|
|
|
changed,
|
|
|
|
|
requestedScripts,
|
|
|
|
|
textPictureCleared: store.graph.getNode(text.id)?.textPicture === null
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
fontManager.ensureFallbackPack = originalEnsureFallbackPack
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
changed: true,
|
2026-07-01 19:19:14 +00:00
|
|
|
requestedScripts: ['cjk-sc'],
|
2026-07-01 19:06:19 +00:00
|
|
|
textPictureCleared: true
|
|
|
|
|
})
|
|
|
|
|
canvas.assertNoErrors()
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-12 13:54:55 +00:00
|
|
|
test('CJK text waits for fallback fonts and repaints after they load', async ({ page }) => {
|
|
|
|
|
const canvas = new CanvasHelper(page)
|
2026-05-16 12:36:37 +00:00
|
|
|
await page.goto('http://localhost:1420/?test&no-chrome&no-rulers')
|
2026-05-12 13:54:55 +00:00
|
|
|
await canvas.waitForInit()
|
|
|
|
|
|
|
|
|
|
const result = await page.evaluate(async () => {
|
|
|
|
|
const store = window.openPencil?.getStore?.()
|
2026-07-17 11:52:35 +00:00
|
|
|
if (!store?.renderer) throw new Error('OpenPencil renderer not initialized')
|
2026-05-12 13:54:55 +00:00
|
|
|
const renderer = store.renderer
|
2026-07-17 13:19:18 +00:00
|
|
|
const response = await fetch('/tests/fixtures/fonts/NotoSansCJK-Test.otf')
|
2026-07-17 11:52:35 +00:00
|
|
|
const fallbackData = await response.arrayBuffer()
|
2026-08-10 12:48:16 +00:00
|
|
|
const fontModuleURL = performance
|
2026-07-17 13:19:18 +00:00
|
|
|
.getEntriesByType('resource')
|
|
|
|
|
.map((entry) => entry.name)
|
|
|
|
|
.find((url) => url.includes('/packages/core/src/text/fonts.ts'))
|
2026-08-10 12:48:16 +00:00
|
|
|
if (!fontModuleURL) throw new Error('Active font manager module not found')
|
|
|
|
|
const { fontManager } = (await import(/* @vite-ignore */ fontModuleURL)) as {
|
2026-07-17 13:19:18 +00:00
|
|
|
fontManager: FontManager
|
|
|
|
|
}
|
2026-07-17 11:52:35 +00:00
|
|
|
const manager = fontManager as typeof fontManager & { cjkFallbackFamilies: string[] }
|
|
|
|
|
const originalFamilies = [...manager.cjkFallbackFamilies]
|
|
|
|
|
const originalEnsureFallbackPack = fontManager.ensureFallbackPack.bind(fontManager)
|
2026-05-12 13:54:55 +00:00
|
|
|
|
2026-07-17 11:52:35 +00:00
|
|
|
let releaseFallback = () => undefined
|
2026-05-12 13:54:55 +00:00
|
|
|
const fallbackGate = new Promise<void>((resolve) => {
|
2026-07-17 11:52:35 +00:00
|
|
|
releaseFallback = resolve
|
2026-05-12 13:54:55 +00:00
|
|
|
})
|
|
|
|
|
manager.cjkFallbackFamilies = []
|
|
|
|
|
|
|
|
|
|
let fallbackRenderCount = 0
|
|
|
|
|
let renderCount = 0
|
|
|
|
|
const originalRender = renderer.renderFromEditorState.bind(renderer)
|
2026-07-17 11:52:35 +00:00
|
|
|
fontManager.ensureFallbackPack = async (scripts = ['cjk', 'arabic']) => {
|
2026-05-12 13:54:55 +00:00
|
|
|
await fallbackGate
|
2026-07-17 13:19:18 +00:00
|
|
|
fontManager.markLoaded('Noto Sans CJK SC', 'Regular', fallbackData)
|
|
|
|
|
fontManager.setCJKFallbackFamily('Noto Sans CJK SC')
|
|
|
|
|
return Object.fromEntries(scripts.map((script) => [script, ['Noto Sans CJK SC']]))
|
2026-05-12 13:54:55 +00:00
|
|
|
}
|
|
|
|
|
renderer.renderFromEditorState = (
|
|
|
|
|
...args: Parameters<typeof renderer.renderFromEditorState>
|
|
|
|
|
) => {
|
|
|
|
|
renderCount += 1
|
|
|
|
|
return originalRender(...args)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 11:52:35 +00:00
|
|
|
const text = store.graph.createNode('TEXT', store.state.currentPageId, {
|
2026-05-12 13:54:55 +00:00
|
|
|
name: 'CJK Regression',
|
|
|
|
|
x: 80,
|
|
|
|
|
y: 80,
|
|
|
|
|
width: 300,
|
|
|
|
|
height: 60,
|
2026-07-17 13:19:18 +00:00
|
|
|
text: '你好世界App',
|
2026-07-17 11:52:35 +00:00
|
|
|
textLanguage: 'zh-Hans',
|
2026-05-12 13:54:55 +00:00
|
|
|
fontSize: 32,
|
|
|
|
|
fontFamily: 'Inter',
|
|
|
|
|
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, visible: true, opacity: 1 }]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await renderer.loadFonts(() => {
|
|
|
|
|
fallbackRenderCount += 1
|
|
|
|
|
renderer.renderFromEditorState(
|
|
|
|
|
store.state,
|
|
|
|
|
store.graph,
|
|
|
|
|
store.textEditor,
|
|
|
|
|
800,
|
|
|
|
|
600,
|
|
|
|
|
false,
|
|
|
|
|
'full'
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
const loadedBeforeFallback = renderer.isNodeFontLoaded(text)
|
2026-07-17 11:52:35 +00:00
|
|
|
releaseFallback()
|
|
|
|
|
await fallbackGate
|
|
|
|
|
await new Promise<void>((resolve) => {
|
2026-05-18 15:41:01 +00:00
|
|
|
setTimeout(resolve, 0)
|
|
|
|
|
})
|
2026-05-12 13:54:55 +00:00
|
|
|
await new Promise(requestAnimationFrame)
|
|
|
|
|
return {
|
|
|
|
|
loadedBeforeFallback,
|
|
|
|
|
loadedAfterFallback: renderer.isNodeFontLoaded(text),
|
2026-07-17 13:19:18 +00:00
|
|
|
readiness: renderer.nodeFontReadiness(text),
|
2026-05-12 13:54:55 +00:00
|
|
|
fallbackRenderCount,
|
|
|
|
|
renderCount
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
2026-07-17 11:52:35 +00:00
|
|
|
manager.cjkFallbackFamilies = originalFamilies
|
|
|
|
|
fontManager.ensureFallbackPack = originalEnsureFallbackPack
|
2026-05-12 13:54:55 +00:00
|
|
|
renderer.renderFromEditorState = originalRender
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.loadedBeforeFallback).toBe(false)
|
2026-07-17 13:19:18 +00:00
|
|
|
expect(result.readiness).toBe('ready')
|
2026-05-12 13:54:55 +00:00
|
|
|
expect(result.loadedAfterFallback).toBe(true)
|
|
|
|
|
expect(result.fallbackRenderCount).toBe(1)
|
|
|
|
|
expect(result.renderCount).toBeGreaterThan(0)
|
|
|
|
|
canvas.assertNoErrors()
|
|
|
|
|
})
|