openpencil/tests/engine/vue/controls/shared-style.test.ts
Danila Poyarkov 86d6a63d0f fix(fig): restore strict parse validation
- 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
2026-07-18 03:58:18 +03:00

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' })
})
})