- Reject corrupt compressed Kiwi chunks instead of accepting raw garbage\n- Tolerate legacy source metadata without editedFields in renderer fallbacks\n- Align font, derived glyph, shared style, renderer mock, and ownership tests with current contracts\n- Restore clean quick and heavy unit quality gates
141 lines
4.6 KiB
TypeScript
141 lines
4.6 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { createEditor } from '@open-pencil/core/editor'
|
|
import { effectiveFigmaRawNodeFields } from '@open-pencil/fig'
|
|
import { getSharedStyles, type Effect, type Fill, type SceneNode } from '@open-pencil/scene-graph'
|
|
|
|
import { sharedStyleDetachPatch, sharedStylePatch } from '#vue/controls/shared-style/model'
|
|
|
|
import { firstPageId, makeSceneGraph } from '#tests/helpers/scene'
|
|
|
|
const red: Fill = {
|
|
type: 'SOLID',
|
|
color: { r: 1, g: 0, b: 0, a: 1 },
|
|
opacity: 1,
|
|
visible: true
|
|
}
|
|
const blue: Fill = {
|
|
type: 'SOLID',
|
|
color: { r: 0, g: 0.3, b: 1, a: 1 },
|
|
opacity: 1,
|
|
visible: true
|
|
}
|
|
|
|
function setSourceId(node: SceneNode, id: string) {
|
|
node.source.id = id
|
|
node.source.format = 'fig'
|
|
}
|
|
|
|
describe('shared style model', () => {
|
|
test('lists internal definitions and applies canonical domain properties', () => {
|
|
const graph = makeSceneGraph()
|
|
const pageId = firstPageId(graph)
|
|
const fillStyle = graph.createNode('RECTANGLE', pageId, {
|
|
name: 'Brand/Primary',
|
|
fills: [red],
|
|
sharedStyleType: 'FILL',
|
|
internalOnly: true
|
|
})
|
|
setSourceId(fillStyle, '1:20')
|
|
const target = graph.createNode('RECTANGLE', pageId, { fills: [blue] })
|
|
|
|
expect(getSharedStyles(graph, 'fill')).toEqual([
|
|
{ id: '1:20', nodeId: fillStyle.id, name: 'Brand/Primary', type: 'FILL' }
|
|
])
|
|
expect(sharedStylePatch('fill', target, '1:20', fillStyle)).toMatchObject({
|
|
fillStyleId: '1:20',
|
|
fills: [red]
|
|
})
|
|
expect(sharedStyleDetachPatch('fill')).toEqual({ fillStyleId: null })
|
|
})
|
|
|
|
test('applies text, effect, and grid style payloads', () => {
|
|
const graph = makeSceneGraph()
|
|
const pageId = firstPageId(graph)
|
|
const target = graph.createNode('TEXT', pageId)
|
|
const textStyle = graph.createNode('TEXT', pageId, {
|
|
fontFamily: 'Inter',
|
|
fontSize: 24,
|
|
fontWeight: 700,
|
|
lineHeight: 32,
|
|
textCase: 'UPPER'
|
|
})
|
|
const effect: Effect = {
|
|
type: 'DROP_SHADOW',
|
|
color: { r: 0, g: 0, b: 0, a: 0.25 },
|
|
offset: { x: 0, y: 4 },
|
|
radius: 8,
|
|
spread: 0,
|
|
visible: true
|
|
}
|
|
const effectStyle = graph.createNode('RECTANGLE', pageId, { effects: [effect] })
|
|
const gridStyle = graph.createNode('FRAME', pageId, {
|
|
layoutGrids: [{ pattern: 'COLUMNS', count: 12, gutterSize: 16, visible: true }]
|
|
})
|
|
|
|
expect(sharedStylePatch('text', target, '1:21', textStyle)).toMatchObject({
|
|
textStyleId: '1:21',
|
|
fontSize: 24,
|
|
fontWeight: 700,
|
|
lineHeight: 32,
|
|
textCase: 'UPPER'
|
|
})
|
|
expect(sharedStylePatch('effect', target, '1:22', effectStyle)).toMatchObject({
|
|
effectStyleId: '1:22',
|
|
effects: [effect]
|
|
})
|
|
expect(sharedStylePatch('grid', target, '1:23', gridStyle)).toMatchObject({
|
|
gridStyleId: '1:23',
|
|
layoutGrids: [{ count: 12 }]
|
|
})
|
|
})
|
|
|
|
test('detaching a reference invalidates only its effective Figma fallback', () => {
|
|
const graph = makeSceneGraph()
|
|
const target = graph.createNode('RECTANGLE', firstPageId(graph), {
|
|
fillStyleId: '1:20',
|
|
effectStyleId: '1:22'
|
|
})
|
|
target.source.fig.rawNodeFields = {
|
|
styleIdForFill: { guid: { sessionID: 1, localID: 20 } },
|
|
styleIdForEffect: { guid: { sessionID: 1, localID: 22 } },
|
|
description: 'Preserve me'
|
|
}
|
|
|
|
graph.updateNode(target.id, { fillStyleId: null })
|
|
|
|
expect(target.source.fig.rawNodeFields).toHaveProperty('styleIdForFill')
|
|
expect(effectiveFigmaRawNodeFields(target)).toEqual({
|
|
styleIdForEffect: { guid: { sessionID: 1, localID: 22 } },
|
|
description: 'Preserve me'
|
|
})
|
|
})
|
|
|
|
test('manual visual edits detach only the matching style and undo restores it', () => {
|
|
const graph = makeSceneGraph()
|
|
const pageId = firstPageId(graph)
|
|
const target = graph.createNode('RECTANGLE', pageId, {
|
|
fills: [red],
|
|
fillStyleId: '1:20',
|
|
effectStyleId: '1:22'
|
|
})
|
|
const editor = createEditor({ graph })
|
|
|
|
editor.updateNodeWithUndo(target.id, { fills: [blue] }, 'Change fills')
|
|
expect(graph.getNode(target.id)).toMatchObject({
|
|
fillStyleId: null,
|
|
effectStyleId: '1:22',
|
|
fills: [blue]
|
|
})
|
|
|
|
editor.undo.undo()
|
|
expect(graph.getNode(target.id)).toMatchObject({ fillStyleId: '1:20', fills: [red] })
|
|
|
|
const text = graph.createNode('TEXT', pageId, { fontSize: 16, textStyleId: '1:21' })
|
|
editor.updateNodeWithUndo(text.id, { fontSize: 24 }, 'Change font size')
|
|
expect(graph.getNode(text.id)).toMatchObject({ fontSize: 24, textStyleId: null })
|
|
editor.undo.undo()
|
|
expect(graph.getNode(text.id)).toMatchObject({ fontSize: 16, textStyleId: '1:21' })
|
|
})
|
|
})
|