test: extend timeout for slow gold-preview fixture tests

Port the timeout fixes from PR #351 that resolved the unit-test CI
timeouts. Add HEAVY_TEST_TIMEOUT_MS (30s) in tests/helpers/test-utils.ts
and apply it to the slow gold-preview.fig fixture tests/hooks:
- clipboard roundtrip
- group reclassification
- glyph blob preservation
- auto-layout text measurement
- render/canvas/cache
This commit is contained in:
Joseph Cumines 2026-06-19 11:05:25 +10:00
parent f862beb08d
commit fa5c072cad
7 changed files with 138 additions and 100 deletions

View file

@ -14,6 +14,7 @@
### Fixes
- Increase per-test timeout for slow `gold-preview.fig` fixture tests (`clipboard roundtrip`, `group reclassification`, `glyph blob preservation`, `auto-layout text measurement`, and `render/canvas/cache`) so they no longer flake on slower CI runners.
- Fix clone operations (duplicate, instance creation, clipboard copy) sharing mutable references with the original — editing fills, strokes, variable bindings, overrides, or vector networks on one no longer corrupts the other.
- Fix instance overrides shallow-copied on clone — override values containing objects are now deep-copied.
- Fix stale variable bindings not cleaned up when fills/strokes arrays shrink — any indexed sub-path is now handled, not just `/color`.

View file

@ -14,6 +14,7 @@ import {
} from '@open-pencil/core'
import { expectDefined } from '#tests/helpers/assert'
import { HEAVY_TEST_TIMEOUT_MS } from '#tests/helpers/test-utils'
describe('gold-preview.fig clipboard roundtrip', () => {
let graph: SceneGraph
@ -29,15 +30,18 @@ describe('gold-preview.fig clipboard roundtrip', () => {
return res
}
beforeAll(async () => {
await initCodec()
const buf = readFileSync('tests/fixtures/gold-preview.fig')
const file = new File([buf], 'gold-preview.fig')
graph = await readFigFile(file)
const page = graph.getPages()[0]
pageId = page.id
topLevelNodes = graph.getChildren(pageId)
})
beforeAll(
async () => {
await initCodec()
const buf = readFileSync('tests/fixtures/gold-preview.fig')
const file = new File([buf], 'gold-preview.fig')
graph = await readFigFile(file)
const page = graph.getPages()[0]
pageId = page.id
topLevelNodes = graph.getChildren(pageId)
},
{ timeout: HEAVY_TEST_TIMEOUT_MS }
)
it('OpenPencil format: zero property differences', () => {
const html = buildOpenPencilClipboardHTML(topLevelNodes, graph)

View file

@ -5,6 +5,7 @@ import { nodeChangeToProps } from '#core/kiwi/fig/node-change/convert'
import { parseFixture } from '#tests/helpers/fig-fixtures'
import { collectAllNodes } from '#tests/helpers/fig-traversal'
import { HEAVY_TEST_TIMEOUT_MS } from '#tests/helpers/test-utils'
describe('Figma group reclassification on import', () => {
test('FRAME with resizeToFit imports as GROUP', () => {
@ -56,11 +57,15 @@ describe('Figma group reclassification on import', () => {
expect(props.nodeType).toBe('FRAME')
})
test('gold-preview.fig fixture imports its groups as GROUP nodes', async () => {
const graph = await parseFixture('gold-preview.fig')
const groups = collectAllNodes(graph).filter((n) => n.type === 'GROUP')
// gold-preview.fig contains real Figma groups (FRAME + resizeToFit) that must
// import as GROUP, not FRAME.
expect(groups.length).toBeGreaterThan(0)
})
test(
'gold-preview.fig fixture imports its groups as GROUP nodes',
{ timeout: HEAVY_TEST_TIMEOUT_MS },
async () => {
const graph = await parseFixture('gold-preview.fig')
const groups = collectAllNodes(graph).filter((n) => n.type === 'GROUP')
// gold-preview.fig contains real Figma groups (FRAME + resizeToFit) that must
// import as GROUP, not FRAME.
expect(groups.length).toBeGreaterThan(0)
}
)
})

View file

@ -7,6 +7,8 @@ import { fontManager } from '@open-pencil/core/text'
import { parseFigBuffer } from '#core/kiwi/fig/parse/core'
import { HEAVY_TEST_TIMEOUT_MS } from '#tests/helpers/test-utils'
const FIXTURES = resolve(import.meta.dir, '../../../../fixtures')
const INTER_ASSETS = resolve(import.meta.dir, '../../../../../packages/core/assets')
@ -44,27 +46,34 @@ function loadInterFonts() {
}
describe('roundtrip: text glyph blobs', () => {
beforeAll(async () => {
await initCodec()
})
beforeAll(
async () => {
await initCodec()
},
{ timeout: HEAVY_TEST_TIMEOUT_MS }
)
test('preserves imported Figma glyph blobs for fallback rendering', async () => {
const fixtureBytes = new Uint8Array(readFileSync(resolve(FIXTURES, 'gold-preview.fig')))
const input = countGlyphBlobs(fixtureBytes)
test(
'preserves imported Figma glyph blobs for fallback rendering',
{ timeout: HEAVY_TEST_TIMEOUT_MS },
async () => {
const fixtureBytes = new Uint8Array(readFileSync(resolve(FIXTURES, 'gold-preview.fig')))
const input = countGlyphBlobs(fixtureBytes)
const graph = await parseFigFile(
fixtureBytes.buffer.slice(
fixtureBytes.byteOffset,
fixtureBytes.byteOffset + fixtureBytes.byteLength
const graph = await parseFigFile(
fixtureBytes.buffer.slice(
fixtureBytes.byteOffset,
fixtureBytes.byteOffset + fixtureBytes.byteLength
)
)
)
const exported = await exportFigFile(graph)
const output = countGlyphBlobs(exported)
const exported = await exportFigFile(graph)
const output = countGlyphBlobs(exported)
expect(input.glyphsWithBlob).toBeGreaterThan(0)
expect(output.glyphsWithBlob).toBe(input.glyphsWithBlob)
expect(output.uniqueGlyphBlobs).toBeLessThanOrEqual(input.uniqueGlyphBlobs)
})
expect(input.glyphsWithBlob).toBeGreaterThan(0)
expect(output.glyphsWithBlob).toBe(input.glyphsWithBlob)
expect(output.uniqueGlyphBlobs).toBeLessThanOrEqual(input.uniqueGlyphBlobs)
}
)
test('deduplicates generated glyph blobs across repeated text', async () => {
loadInterFonts()

View file

@ -6,6 +6,7 @@ import { createEditorStore } from '@/app/editor/session'
import { getNodeOrThrow } from '#tests/helpers/assert'
import { autoFrame, loadFixtureGraph, pageId, rect } from '#tests/helpers/layout'
import { HEAVY_TEST_TIMEOUT_MS } from '#tests/helpers/test-utils'
describe('text measurement', () => {
test('derived text layout preserves imported auto-layout text bounds during measurement', () => {
@ -85,80 +86,91 @@ describe('text measurement', () => {
expect(graph.getNode(tabs.id)?.height).toBe(62)
})
test('opening imported fig keeps stored text bounds before CanvasKit measurement', async () => {
const graph = await loadFixtureGraph('gold-preview.fig')
const store = createEditorStore(graph)
const title = [...store.graph.getAllNodes()].find(
(node) => node.type === 'TEXT' && node.text === "World's largest"
)
const subtitle = [...store.graph.getAllNodes()].find(
(node) =>
node.type === 'TEXT' && node.text === 'Preline UI Figma - crafted with Tailwind CSS styles'
)
const description = [...store.graph.getAllNodes()].find(
(node) =>
node.type === 'TEXT' &&
node.text.startsWith('Preline UI Figma is the largest free design system for Figma')
)
test(
'opening imported fig keeps stored text bounds before CanvasKit measurement',
{ timeout: HEAVY_TEST_TIMEOUT_MS },
async () => {
const graph = await loadFixtureGraph('gold-preview.fig')
const store = createEditorStore(graph)
const title = [...store.graph.getAllNodes()].find(
(node) => node.type === 'TEXT' && node.text === "World's largest"
)
const subtitle = [...store.graph.getAllNodes()].find(
(node) =>
node.type === 'TEXT' &&
node.text === 'Preline UI Figma - crafted with Tailwind CSS styles'
)
const description = [...store.graph.getAllNodes()].find(
(node) =>
node.type === 'TEXT' &&
node.text.startsWith('Preline UI Figma is the largest free design system for Figma')
)
if (!title || !subtitle || !description) {
throw new Error('Expected imported text nodes in gold-preview.fig')
if (!title || !subtitle || !description) {
throw new Error('Expected imported text nodes in gold-preview.fig')
}
expect(title.width).toBe(444)
expect(title.height).toBe(73)
expect(subtitle.width).toBe(439)
expect(subtitle.height).toBe(22)
expect(description.width).toBe(878)
expect(description.height).toBe(60)
await store.switchPage(store.graph.getPages()[0].id)
expect(store.graph.getNode(title.id)?.width).toBe(444)
expect(store.graph.getNode(title.id)?.height).toBe(73)
expect(store.graph.getNode(subtitle.id)?.width).toBe(439)
expect(store.graph.getNode(subtitle.id)?.height).toBe(22)
expect(store.graph.getNode(description.id)?.width).toBe(878)
expect(store.graph.getNode(description.id)?.height).toBe(60)
}
)
expect(title.width).toBe(444)
expect(title.height).toBe(73)
expect(subtitle.width).toBe(439)
expect(subtitle.height).toBe(22)
expect(description.width).toBe(878)
expect(description.height).toBe(60)
test(
'imported nested instance layout keeps hidden sibling offsets stable',
{ timeout: HEAVY_TEST_TIMEOUT_MS },
async () => {
const graph = await loadFixtureGraph('gold-preview.fig')
const previewRoot = graph.getChildren(graph.getPages()[0].id)[0]
const wysiwygEditor = graph
.getChildren(previewRoot.id)
.find((node) => node.name === '_WYSIWYG-editor')
const toolbarVariant = wysiwygEditor
? graph
.getChildren(wysiwygEditor.id)
.find((node) => node.name === '_on-text-WYSIWYG-toolbar')
: undefined
const toolbarRow = toolbarVariant
? graph.getChildren(toolbarVariant.id).find((node) => node.name === 'Toolbar')
: undefined
const hiddenInput = toolbarRow
? graph.getChildren(toolbarRow.id).find((node) => node.name === 'Input')
: undefined
const visibleToolbar = toolbarRow
? graph
.getChildren(toolbarRow.id)
.find((node) => node.name === 'Toolbar' && node.id !== toolbarRow.id)
: undefined
await store.switchPage(store.graph.getPages()[0].id)
if (!toolbarRow || !hiddenInput || !visibleToolbar) {
throw new Error('Expected imported WYSIWYG toolbar nodes in gold-preview.fig')
}
expect(store.graph.getNode(title.id)?.width).toBe(444)
expect(store.graph.getNode(title.id)?.height).toBe(73)
expect(store.graph.getNode(subtitle.id)?.width).toBe(439)
expect(store.graph.getNode(subtitle.id)?.height).toBe(22)
expect(store.graph.getNode(description.id)?.width).toBe(878)
expect(store.graph.getNode(description.id)?.height).toBe(60)
})
expect(hiddenInput.visible).toBe(false)
// x=8 is the Figma-exported fixture value (verified via fixture inspection 2026-05-03).
// The old assertion of x=298 was incorrect — it was a post-layout computed value from
// an older layout implementation. Post-layout, left-aligned HUG content retains x=8.
expect(visibleToolbar.x).toBe(8)
test('imported nested instance layout keeps hidden sibling offsets stable', async () => {
const graph = await loadFixtureGraph('gold-preview.fig')
const previewRoot = graph.getChildren(graph.getPages()[0].id)[0]
const wysiwygEditor = graph
.getChildren(previewRoot.id)
.find((node) => node.name === '_WYSIWYG-editor')
const toolbarVariant = wysiwygEditor
? graph.getChildren(wysiwygEditor.id).find((node) => node.name === '_on-text-WYSIWYG-toolbar')
: undefined
const toolbarRow = toolbarVariant
? graph.getChildren(toolbarVariant.id).find((node) => node.name === 'Toolbar')
: undefined
const hiddenInput = toolbarRow
? graph.getChildren(toolbarRow.id).find((node) => node.name === 'Input')
: undefined
const visibleToolbar = toolbarRow
? graph
.getChildren(toolbarRow.id)
.find((node) => node.name === 'Toolbar' && node.id !== toolbarRow.id)
: undefined
setTextMeasurer(null)
computeAllLayouts(graph, graph.getPages()[0].id)
if (!toolbarRow || !hiddenInput || !visibleToolbar) {
throw new Error('Expected imported WYSIWYG toolbar nodes in gold-preview.fig')
expect(graph.getNode(visibleToolbar.id)?.x).toBe(8)
expect(graph.getNode(visibleToolbar.id)?.y).toBe(8)
}
expect(hiddenInput.visible).toBe(false)
// x=8 is the Figma-exported fixture value (verified via fixture inspection 2026-05-03).
// The old assertion of x=298 was incorrect — it was a post-layout computed value from
// an older layout implementation. Post-layout, left-aligned HUG content retains x=8.
expect(visibleToolbar.x).toBe(8)
setTextMeasurer(null)
computeAllLayouts(graph, graph.getPages()[0].id)
expect(graph.getNode(visibleToolbar.id)?.x).toBe(8)
expect(graph.getNode(visibleToolbar.id)?.y).toBe(8)
})
)
test('WIDTH_AND_HEIGHT text uses measured width in centered layout', () => {
const graph = new SceneGraph()

View file

@ -1,4 +1,4 @@
import { beforeAll, describe, expect, test } from 'bun:test'
import { beforeAll, describe, expect, setDefaultTimeout, test } from 'bun:test'
import { readFileSync } from 'node:fs'
import {
@ -15,6 +15,9 @@ import { initCanvasKit } from '#cli/headless'
import { expectDefined } from '#tests/helpers/assert'
import { repoPath } from '#tests/helpers/paths'
import { HEAVY_TEST_TIMEOUT_MS } from '#tests/helpers/test-utils'
setDefaultTimeout(HEAVY_TEST_TIMEOUT_MS)
let graph: SceneGraph
let movingNodeId: string

View file

@ -8,3 +8,7 @@ export const runsHeavyTests = process.env.BUN_HEAVY_TESTS
: true
export const heavy = describe.if(runsHeavyTests)
// Default per-test timeout in CI is often too short for fixture I/O tests that parse/export
// real .fig files. Use this timeout for tests that are bounded by fixture parsing speed.
export const HEAVY_TEST_TIMEOUT_MS = 30_000