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:
parent
f862beb08d
commit
fa5c072cad
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue