openpencil/tests/e2e/canvas/typography-depth-visual.spec.ts
Danila Poyarkov dceae73c6e perf(text): preserve coverage across paragraph eviction
Keep successful glyph coverage weakly owned by source nodes rather than repeating shaping when dense scans evict native paragraphs. Preserve font/input invalidation and bound pending ID invalidations without increasing native cache limits.

Add a dense-preview regression with exact raster comparison and count paragraph builds across all canvas renderers. The previous compiled build rebuilds 3,600 paragraphs over three held frames; the fixed build rebuilds none.
2026-09-15 16:32:07 +03:00

121 lines
4 KiB
TypeScript

import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
import { probeParagraphBuilds } from '#tests/helpers/canvas/text-preparation'
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
test('text case vertical alignment and ending truncation', async () => {
const previewId = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
const cases = [
{ label: 'uppercase text', textCase: 'UPPER' as const, vertical: 'TOP' as const },
{ label: 'centered Title', textCase: 'TITLE' as const, vertical: 'CENTER' as const },
{ label: 'bottom text', textCase: 'LOWER' as const, vertical: 'BOTTOM' as const }
]
for (const [index, item] of cases.entries()) {
const x = 70 + index * 240
store.graph.createNode('RECTANGLE', pageId, {
x,
y: 90,
width: 200,
height: 130,
fills: [
{
type: 'SOLID',
color: { r: 0.92, g: 0.94, b: 0.98, a: 1 },
opacity: 1,
visible: true
}
]
})
store.graph.createNode('TEXT', pageId, {
x: x + 12,
y: 102,
width: 176,
height: 106,
text: item.label,
fontSize: 20,
lineHeight: 36,
textCase: item.textCase,
textAlignVertical: item.vertical
})
}
store.graph.createNode('RECTANGLE', pageId, {
x: 70,
y: 270,
width: 300,
height: 80,
fills: [
{
type: 'SOLID',
color: { r: 0.98, g: 0.91, b: 0.82, a: 1 },
opacity: 1,
visible: true
}
]
})
store.graph.createNode('TEXT', pageId, {
x: 82,
y: 280,
width: 276,
height: 60,
text: 'This paragraph demonstrates a deterministic two-line ending truncation with an ellipsis.',
fontSize: 17,
lineHeight: 22,
textTruncation: 'ENDING',
maxLines: 2,
textAlignHorizontal: 'JUSTIFIED'
})
const preview = store.graph.createNode('RECTANGLE', pageId, { x: -10000, width: 1, height: 1 })
store.clearSelection()
store.requestRender()
return preview.id
})
await editor.canvas.waitForRender()
await editor.page.waitForTimeout(300)
editor.canvas.assertNoErrors()
expect(await editor.canvas.screenshotCanvasRegion()).toMatchSnapshot('typography-depth.png')
// Compare cold and warm text on the same direct-render path, not retained vs direct rasterization.
await editor.page.evaluate((id) => {
const store = window.openPencil?.getStore?.()
if (!store?.renderer) throw new Error('Renderer unavailable')
store.renderer.textPreparationCache.clear()
store.graph.updateNodePreview(id, { x: -10004 })
store.requestRepaint()
}, previewId)
await editor.canvas.waitForRender()
const reference = await editor.canvas.screenshotCanvasRegion()
const preparations = await probeParagraphBuilds(editor.page)
try {
for (let step = 0; step < 3; step++) {
await editor.page.evaluate(
({ id, x }) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('Editor unavailable')
store.graph.updateNodePreview(id, { x })
store.requestRepaint()
},
{ id: previewId, x: -10001 - step }
)
await editor.canvas.waitForRender()
}
const actual = await editor.canvas.screenshotCanvasRegion()
if (!actual.equals(reference)) {
await test.info().attach('cold-text', { body: reference, contentType: 'image/png' })
await test.info().attach('warm-text', { body: actual, contentType: 'image/png' })
}
expect(actual.equals(reference)).toBe(true)
expect(await preparations.evaluate((probe) => probe.count())).toBe(0)
} finally {
try {
await preparations.evaluate((probe) => probe.restore())
} finally {
await preparations.dispose()
}
}
})