openpencil/tests/engine/io/fig/roundtrip/export-settings.test.ts
2026-06-30 10:55:20 +03:00

154 lines
4.9 KiB
TypeScript

import { beforeAll, describe, expect, test } from 'bun:test'
import {
exportFigFile,
importNodeChanges,
initCodec,
parseFigFile,
SceneGraph,
type NodeChange
} from '@open-pencil/core'
import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse'
import { MAX_EXPORT_SCALE } from '@open-pencil/scene-graph'
function decodeExport(bytes: Uint8Array) {
return parseFigBuffer(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength))
}
function doc(): NodeChange {
return {
guid: { sessionID: 0, localID: 0 },
type: 'DOCUMENT',
name: 'Document',
visible: true,
opacity: 1,
phase: 'CREATED',
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
} as NodeChange
}
function canvas(): NodeChange {
return {
guid: { sessionID: 0, localID: 1 },
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
type: 'CANVAS',
name: 'Page 1',
visible: true,
opacity: 1,
phase: 'CREATED',
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
} as NodeChange
}
function frame(overrides: Partial<NodeChange> = {}): NodeChange {
return {
guid: { sessionID: 1, localID: 10 },
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' },
type: 'FRAME',
name: 'Export settings frame',
visible: true,
opacity: 1,
phase: 'CREATED',
size: { x: 100, y: 100 },
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
...overrides
} as NodeChange
}
describe('fig roundtrip export settings', () => {
beforeAll(async () => {
await initCodec()
})
test('persists per-node export settings losslessly through plugin data', async () => {
const graph = new SceneGraph()
const page = graph.getPages()[0]
const rect = graph.createNode('RECTANGLE', page.id, {
name: 'Exportable rect',
exportSettings: [
{ scale: 2, format: 'png' },
{ scale: 3, format: 'webp' }
]
})
const reimported = await parseFigFile((await exportFigFile(graph)).buffer as ArrayBuffer)
const reimportedRect = [...reimported.getAllNodes()].find((node) => node.name === rect.name)
expect(reimportedRect?.exportSettings).toEqual(rect.exportSettings)
})
test('maps native Figma PNG content-scale settings when plugin data is absent', () => {
const graph = importNodeChanges([
doc(),
canvas(),
frame({
exportSettings: [
{
imageType: 'PNG',
constraint: { type: 'CONTENT_SCALE', value: 2 }
},
{
imageType: 'GIF',
constraint: { type: 'CONTENT_SCALE', value: 4 }
}
]
})
])
const importedFrame = [...graph.getAllNodes()].find(
(node) => node.name === 'Export settings frame'
)
expect(importedFrame?.exportSettings).toEqual([{ scale: 2, format: 'png' }])
})
test('does not serialize export settings plugin data for a default node', async () => {
const graph = new SceneGraph()
const page = graph.getPages()[0]
const rect = graph.createNode('RECTANGLE', page.id, { name: 'Default export settings rect' })
const decoded = decodeExport(await exportFigFile(graph))
const exportedRect = decoded.nodeChanges.find((nodeChange) => nodeChange.name === rect.name)
expect(
exportedRect?.pluginData?.some(
(entry) => entry.pluginID === 'open-pencil' && entry.key === 'exportSettings'
) ?? false
).toBe(false)
})
test('does not resurrect native export settings after the user clears all rows', async () => {
// Imported node carries NATIVE export settings (no plugin override).
const graph = importNodeChanges([
doc(),
canvas(),
frame({
exportSettings: [{ imageType: 'PNG', constraint: { type: 'CONTENT_SCALE', value: 2 } }]
})
])
const node = [...graph.getAllNodes()].find((n) => n.name === 'Export settings frame')
if (!node) throw new Error('imported frame not found')
expect(node.exportSettings).toEqual([{ scale: 2, format: 'png' }])
// User removes every export row; the raw native settings must be dropped so they
// don't come back via the import fallback on reopen.
graph.updateNode(node.id, { exportSettings: [] })
expect(node.source.fig.rawNodeFields?.exportSettings).toBeUndefined()
const reimported = await parseFigFile((await exportFigFile(graph)).buffer as ArrayBuffer)
const reNode = [...reimported.getAllNodes()].find((n) => n.name === 'Export settings frame')
expect(reNode?.exportSettings).toEqual([])
})
test('clamps an out-of-range native export scale at the import boundary', () => {
const graph = importNodeChanges([
doc(),
canvas(),
frame({
exportSettings: [{ imageType: 'PNG', constraint: { type: 'CONTENT_SCALE', value: 999999 } }]
})
])
const node = [...graph.getAllNodes()].find((n) => n.name === 'Export settings frame')
expect(node?.exportSettings).toEqual([{ scale: MAX_EXPORT_SCALE, format: 'png' }])
})
})