- Model imported fill, stroke, text, effect, and grid style definitions and references - Add SDK and app selectors with mixed-selection batching, detach-on-edit, and undo restoration - Preserve style definitions through .fig export and extracted subgraphs - Document the shared-style composable and update compatibility coverage
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import { beforeAll, describe, expect, test } from 'bun:test'
|
|
|
|
import { exportFigFile, initCodec } from '@open-pencil/core'
|
|
import { parseFigBuffer } from '@open-pencil/kiwi/fig/parse'
|
|
import { SceneGraph } from '@open-pencil/scene-graph'
|
|
|
|
import { sceneNodeToKiwi } from '#core/kiwi/fig/node-change/serialize'
|
|
|
|
function serialize(graph: SceneGraph, nodeId: string) {
|
|
const node = graph.getNode(nodeId)
|
|
if (!node) throw new Error('Expected scene node')
|
|
return sceneNodeToKiwi(node, { sessionID: 1, localID: 1 }, 0, { value: 20 }, graph, [])[0]
|
|
}
|
|
|
|
describe('Figma shared style export', () => {
|
|
beforeAll(async () => {
|
|
await initCodec()
|
|
})
|
|
test('exports every modeled style reference and promoted layout grids', () => {
|
|
const graph = new SceneGraph()
|
|
const page = graph.getPages()[0]
|
|
const node = graph.createNode('FRAME', page.id, {
|
|
fillStyleId: '1:10',
|
|
strokeStyleId: '1:11',
|
|
textStyleId: '1:12',
|
|
effectStyleId: '1:13',
|
|
gridStyleId: '1:14',
|
|
layoutGrids: [{ pattern: 'COLUMNS', count: 12, gutterSize: 16, visible: true }]
|
|
})
|
|
|
|
expect(serialize(graph, node.id)).toMatchObject({
|
|
styleIdForFill: { guid: { sessionID: 1, localID: 10 } },
|
|
styleIdForStrokeFill: { guid: { sessionID: 1, localID: 11 } },
|
|
styleIdForText: { guid: { sessionID: 1, localID: 12 } },
|
|
styleIdForEffect: { guid: { sessionID: 1, localID: 13 } },
|
|
styleIdForGrid: { guid: { sessionID: 1, localID: 14 } },
|
|
layoutGrids: [{ pattern: 'COLUMNS', count: 12, gutterSize: 16, visible: true }]
|
|
})
|
|
})
|
|
|
|
test('exports internal style definitions with their style type', () => {
|
|
const graph = new SceneGraph()
|
|
const page = graph.getPages()[0]
|
|
const style = graph.createNode('RECTANGLE', page.id, {
|
|
name: 'Brand/Primary',
|
|
sharedStyleType: 'FILL',
|
|
internalOnly: true
|
|
})
|
|
style.source.id = '1:10'
|
|
|
|
expect(serialize(graph, style.id)).toMatchObject({
|
|
guid: { sessionID: 1, localID: 10 },
|
|
name: 'Brand/Primary',
|
|
styleType: 'FILL'
|
|
})
|
|
})
|
|
|
|
test('includes referenced definitions in a complete .fig export', async () => {
|
|
const graph = new SceneGraph()
|
|
const page = graph.getPages()[0]
|
|
const style = graph.createNode('RECTANGLE', page.id, {
|
|
name: 'Brand/Primary',
|
|
sharedStyleType: 'FILL',
|
|
internalOnly: true
|
|
})
|
|
style.source.id = '1:10'
|
|
graph.createNode('RECTANGLE', page.id, { name: 'Styled target', fillStyleId: '1:10' })
|
|
|
|
const bytes = await exportFigFile(graph)
|
|
const parsed = parseFigBuffer(
|
|
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
|
|
)
|
|
const definition = parsed.nodeChanges.find((change) => change.styleType === 'FILL')
|
|
const target = parsed.nodeChanges.find((change) => change.name === 'Styled target')
|
|
|
|
expect(definition).toMatchObject({ name: 'Brand/Primary', styleType: 'FILL' })
|
|
expect(target?.styleIdForFill).toEqual({ guid: { sessionID: 1, localID: 10 } })
|
|
})
|
|
})
|