openpencil/tests/engine/render/canvas/renderer-lifecycle.test.ts
Danila Poyarkov 0f64cecc49
fix(text): finalize font readiness and label shaping (#593)
* fix(text): prepare browser fonts atomically

- Fetch approved Fontsource resources directly in browsers with bounded responses and retryable provider failures
- Limit page font resolution concurrency and retain live Inter substitution paragraphs after baked glyph invalidation
- Shape frame, section, and component labels through a bounded native Paragraph cache
- Cover real Geist, Geist Mono, and Roboto Mono browser loading plus substitution and cache lifecycle behavior

* test(canvas): cover substitution and label shaping

- Capture baked missing-font text before editing, live Inter substitution on first input, and visible undo output
- Assert text-picture and derived-glyph invalidation with finalized substituted readiness
- Cover shaped frame, rotated frame, section, component, component-set, ellipsis, kerning, and zoom label presentation

* fix(text): preserve same-origin fetches during font resolution

- Route same-origin application and fixture requests through native fetch while Unifont temporarily proxies global HTTP requests
- Keep the external provider HTTPS allowlist enforced for cross-origin font resources
- Cover the production race discovered while loading gold-preview.fig during provider initialization

* fix(text): preserve substituted path glyphs

- Keep imported derived curved glyphs for text-on-path layers when exact fonts finalize as substituted
- Cover substituted path rendering through the runtime renderer and refresh the corrected visual oracle
- Update shaped Inter measurement badges and merged typography panel snapshots after visual inspection
- Search virtualized font catalogs explicitly and restore the canvas screenshot helper used by broad E2E coverage

* fix(text): use browser font transport without desktop warning

- Keep the browser provider implementation aligned with current settings behavior after splitting from preparation

* test(text): inject same-origin browser font context

- Keep the transport test deterministic outside a Window global

* fix(text): preserve final browser font transport hardening

- Carry the same-origin large-document bypass and bounded cross-origin provider checks from the preserved integration snapshot

* fix(text): initialize font transport outside Window contexts

- Keep unit and headless module imports safe while browsers use their current origin

* test(text): satisfy async visual fixture contract

- Return from the resolver setup continuation after requesting render

* test(canvas): include paragraph cache lifecycle

* fix(text): cancel queued browser font loads

- Race serialized provider work against preparation cancellation\n- Release queue slots after cancelled waiters without disturbing active requests\n- Reuse one section-title paragraph cache entry for measurement and drawing

* fix(text): close font queue cancellation races

- Release reserved proxy queue slots when cancellation lands after queue acquisition\n- Skip paragraph work for zero-width section labels

* fix(text): abort active provider resolution

- Race active provider resolution against its preparation signal and restore the temporary fetch proxy promptly\n- Forward cancellation through proxied provider requests\n- Align the renderer font-readiness facade with substituted text
2026-08-30 14:33:32 +03:00

77 lines
2.6 KiB
TypeScript

import { expect, mock, test } from 'bun:test'
import type { Font, Paint, Surface } from 'canvaskit-wasm'
import type { SkiaRenderer } from '#core/canvas/renderer'
import { destroyRenderer } from '#core/canvas/renderer/lifecycle'
function deletable<T>() {
return { delete: mock() } as T & { delete: ReturnType<typeof mock> }
}
function createRenderer() {
const renderer: Partial<SkiaRenderer> = {
destroyed: false,
imageCache: new Map(),
vectorPathCache: new Map(),
vectorStrokePathCache: new Map(),
vectorStrokeOutlineCache: new Map(),
fillGeometryCache: new Map(),
strokeGeometryCache: new Map(),
glyphSilhouetteCache: new Map(),
fillPaint: deletable<Paint>(),
strokePaint: deletable<Paint>(),
selectionPaint: deletable<Paint>(),
parentOutlinePaint: deletable<Paint>(),
snapPaint: deletable<Paint>(),
auxFill: deletable<Paint>(),
auxStroke: deletable<Paint>(),
opacityPaint: deletable<Paint>(),
effectLayerPaint: deletable<Paint>(),
textFont: deletable<Font>(),
labelFont: deletable<Font>(),
sizeFont: deletable<Font>(),
sectionTitleFont: deletable<Font>(),
componentLabelFont: deletable<Font>(),
fontMgr: null,
fontProvider: null,
fontsLoaded: true,
rulerBgPaint: deletable<Paint>(),
rulerTickPaint: deletable<Paint>(),
rulerTextPaint: deletable<Paint>(),
rulerHlPaint: deletable<Paint>(),
rulerBadgePaint: deletable<Paint>(),
rulerLabelPaint: deletable<Paint>(),
penPathPaint: deletable<Paint>(),
penLiveStrokePaint: deletable<Paint>(),
penHandlePaint: deletable<Paint>(),
penVertexFill: deletable<Paint>(),
penVertexStroke: deletable<Paint>(),
imageFilterCache: new Map(),
maskFilterCache: new Map(),
nodePictureCache: new Map(),
labelParagraphCache: { clear: mock() } as SkiaRenderer['labelParagraphCache'],
subtreePictureCache: new Map(),
scenePicture: null,
sceneBacking: null,
sceneBackingBuild: null,
_flashPaint: null,
profiler: { destroy: mock() } as Partial<SkiaRenderer['profiler']> as SkiaRenderer['profiler'],
surface: deletable<Surface>()
}
return renderer as SkiaRenderer
}
test('destroyRenderer deletes all renderer-owned paints and label fonts', () => {
const renderer = createRenderer()
const parentOutlinePaint = renderer.parentOutlinePaint
const sectionTitleFont = renderer.sectionTitleFont
const componentLabelFont = renderer.componentLabelFont
destroyRenderer(renderer)
expect(parentOutlinePaint.delete).toHaveBeenCalled()
expect(sectionTitleFont?.delete).toHaveBeenCalled()
expect(componentLabelFont?.delete).toHaveBeenCalled()
})