183 lines
6.3 KiB
TypeScript
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)
|
|
})
|
|
})
|