openpencil/tests/engine/io/fig/roundtrip/variables.test.ts
2026-05-24 12:15:29 +03:00

183 lines
6.3 KiB
TypeScript

import { describe, expect, setDefaultTimeout, test } from 'bun:test'
import {
exportFigFile,
FigmaAPI,
initCodec,
parseFigFile,
SceneGraph,
type Color
} from '@open-pencil/core'
import { expectDefined } from '#tests/helpers/assert'
import { parseFixture } from '#tests/helpers/fig-fixtures'
import { runsHeavyTests } from '#tests/helpers/test-utils'
setDefaultTimeout(60_000)
describe('variable roundtrip', () => {
test('variables and collections survive export → re-import', async () => {
await initCodec()
const graph = new SceneGraph()
const col = graph.createCollection('Design Tokens')
graph.createVariable('color/primary', 'COLOR', col.id, { r: 0.23, g: 0.51, b: 0.96, a: 1 })
graph.createVariable('spacing/base', 'FLOAT', col.id, 8)
graph.createVariable('visible', 'BOOLEAN', col.id, true)
graph.createVariable('label', 'STRING', col.id, 'Hello')
const exported = await exportFigFile(graph)
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
expect(reimported.variables.size).toBe(4)
expect(reimported.variableCollections.size).toBe(1)
const reimportedCol = [...reimported.variableCollections.values()][0]
expect(reimportedCol.name).toBe('Design Tokens')
expect(reimportedCol.variableIds).toHaveLength(4)
const vars = [...reimported.variables.values()]
const colorVar = vars.find((v) => v.name === 'color/primary')
expect(colorVar).toBeDefined()
expect(expectDefined(colorVar, 'colorVar').type).toBe('COLOR')
const colorVal = Object.values(colorVar.valuesByMode)[0] as Color
expect(colorVal.r).toBeCloseTo(0.23, 1)
const floatVar = vars.find((v) => v.name === 'spacing/base')
expect(floatVar).toBeDefined()
expect(expectDefined(floatVar, 'floatVar').type).toBe('FLOAT')
expect(Object.values(floatVar.valuesByMode)[0]).toBe(8)
const boolVar = vars.find((v) => v.name === 'visible')
expect(boolVar).toBeDefined()
expect(expectDefined(boolVar, 'boolVar').type).toBe('BOOLEAN')
expect(Object.values(boolVar.valuesByMode)[0]).toBe(true)
const strVar = vars.find((v) => v.name === 'label')
expect(strVar).toBeDefined()
expect(expectDefined(strVar, 'strVar').type).toBe('STRING')
expect(Object.values(strVar.valuesByMode)[0]).toBe('Hello')
})
test('variable bindings survive export → re-import', async () => {
await initCodec()
const graph = new SceneGraph()
const col = graph.createCollection('Tokens')
const floatVar = graph.createVariable('radius', 'FLOAT', col.id, 12)
const fillVar = graph.createVariable('surface', 'COLOR', col.id, { r: 1, g: 1, b: 1, a: 1 })
const strokeVar = graph.createVariable('border', 'COLOR', col.id, {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1
})
const page = graph.getPages()[0]
const rect = graph.createNode('RECTANGLE', page.id, {
name: 'Bound Rect',
width: 100,
height: 100,
cornerRadius: 12,
fills: [
{
type: 'SOLID',
color: { r: 1, g: 1, b: 1, a: 1 },
opacity: 1,
visible: true,
blendMode: 'NORMAL'
}
],
strokes: [
{
color: { r: 0.1, g: 0.2, b: 0.3, a: 1 },
weight: 1,
opacity: 1,
visible: true,
align: 'INSIDE',
cap: 'NONE',
join: 'MITER',
dashPattern: []
}
]
})
graph.bindVariable(rect.id, 'cornerRadius', floatVar.id)
graph.bindVariable(rect.id, 'fills/0/color', fillVar.id)
graph.bindVariable(rect.id, 'strokes/0/color', strokeVar.id)
const exported = await exportFigFile(graph)
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
const reimportedRect = [...reimported.getAllNodes()].find((n) => n.name === 'Bound Rect')
expect(reimportedRect).toBeDefined()
expect(Object.keys(expectDefined(reimportedRect, 'reimportedRect').boundVariables)).toContain(
'cornerRadius'
)
expect(Object.keys(reimportedRect.boundVariables)).toContain('fills/0/color')
expect(Object.keys(reimportedRect.boundVariables)).toContain('strokes/0/color')
})
test.if(runsHeavyTests)(
'material3.fig variables survive round-trip',
async () => {
const original = await parseFixture('material3.fig')
const exported = await exportFigFile(original)
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
expect(reimported.variables.size).toBe(original.variables.size)
expect(reimported.variableCollections.size).toBeGreaterThanOrEqual(
[...original.variableCollections.values()].filter((c) => c.variableIds.length > 0).length
)
},
120_000
)
test('pluginID casing is consistent across full codec pipeline', async () => {
await initCodec()
// Create a graph with multiple nodes having pluginData entries
const graph = new SceneGraph()
const api = new FigmaAPI(graph)
const frame = api.createFrame()
frame.name = 'PluginID Test'
frame.setPluginData('key1', 'value1')
frame.setPluginData('key2', 'value2')
const rect = api.createRectangle()
rect.name = 'Plugin Rect'
rect.setPluginData('testKey', 'testValue')
// Round-trip through the codec
const exported = await exportFigFile(graph)
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
// Find all nodes with pluginData
let nodesWithPluginData = 0
let totalEntries = 0
for (const node of reimported.getAllNodes()) {
if (node.pluginData && node.pluginData.length > 0) {
nodesWithPluginData++
for (const entry of node.pluginData) {
totalEntries++
// Every entry MUST use pluginId (lowercase d, matching SceneGraph type)
expect(entry).toHaveProperty('pluginId')
expect(typeof entry.pluginId).toBe('string')
expect(entry.pluginId.length).toBeGreaterThan(0)
}
}
// Also check pluginRelaunchData entries
if (node.pluginRelaunchData && node.pluginRelaunchData.length > 0) {
for (const entry of node.pluginRelaunchData) {
expect(entry).toHaveProperty('pluginId')
expect(typeof entry.pluginId).toBe('string')
}
}
}
// Should have at least 2 nodes with plugin data (frame + rect)
expect(nodesWithPluginData).toBeGreaterThanOrEqual(2)
expect(totalEntries).toBeGreaterThanOrEqual(3)
})
})