- Import and round-trip per-node variable mode overrides - Resolve fill and stroke aliases through the nearest mode scope - Preserve mode maps across cloning and instance synchronization
321 lines
8.3 KiB
TypeScript
321 lines
8.3 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { SceneGraph } from '@open-pencil/core'
|
|
|
|
describe('Variables', () => {
|
|
function pageId(graph: SceneGraph): string {
|
|
return graph.getPages()[0]?.id ?? ''
|
|
}
|
|
|
|
test('add and resolve color variable', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Colors',
|
|
modes: [{ modeId: 'm1', name: 'Light' }],
|
|
defaultModeId: 'm1',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v1',
|
|
name: 'Primary',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: { r: 0, g: 0.5, b: 1, a: 1 } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
const color = graph.resolveColorVariable('v1')
|
|
expect(color).toEqual({ r: 0, g: 0.5, b: 1, a: 1 })
|
|
})
|
|
|
|
test('resolve number variable', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Spacing',
|
|
modes: [{ modeId: 'm1', name: 'Default' }],
|
|
defaultModeId: 'm1',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v1',
|
|
name: 'spacing/md',
|
|
type: 'FLOAT',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: 16 },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
expect(graph.resolveNumberVariable('v1')).toBe(16)
|
|
})
|
|
|
|
test('resolve alias variable', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Tokens',
|
|
modes: [{ modeId: 'm1', name: 'Light' }],
|
|
defaultModeId: 'm1',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v1',
|
|
name: 'Blue/500',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: { r: 0, g: 0, b: 1, a: 1 } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
graph.addVariable({
|
|
id: 'v2',
|
|
name: 'Primary',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: { aliasId: 'v1' } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
expect(graph.resolveColorVariable('v2')).toEqual({ r: 0, g: 0, b: 1, a: 1 })
|
|
})
|
|
|
|
test('mode switching changes resolved value', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Theme',
|
|
modes: [
|
|
{ modeId: 'light', name: 'Light' },
|
|
{ modeId: 'dark', name: 'Dark' }
|
|
],
|
|
defaultModeId: 'light',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v1',
|
|
name: 'bg',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: {
|
|
light: { r: 1, g: 1, b: 1, a: 1 },
|
|
dark: { r: 0, g: 0, b: 0, a: 1 }
|
|
},
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
expect(graph.resolveColorVariable('v1')).toEqual({ r: 1, g: 1, b: 1, a: 1 })
|
|
graph.setActiveMode('col1', 'dark')
|
|
expect(graph.resolveColorVariable('v1')).toEqual({ r: 0, g: 0, b: 0, a: 1 })
|
|
})
|
|
|
|
test('node variable modes inherit from the nearest ancestor', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Theme',
|
|
modes: [
|
|
{ modeId: 'light', name: 'Light' },
|
|
{ modeId: 'dark', name: 'Dark' }
|
|
],
|
|
defaultModeId: 'light',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v1',
|
|
name: 'bg',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: {
|
|
light: { r: 1, g: 1, b: 1, a: 1 },
|
|
dark: { r: 0, g: 0, b: 0, a: 1 }
|
|
},
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
const page = graph.addPage('Page')
|
|
const frame = graph.createNode('FRAME', page.id, { variableModes: { col1: 'dark' } })
|
|
const child = graph.createNode('RECTANGLE', frame.id)
|
|
const nestedOverride = graph.createNode('RECTANGLE', frame.id, {
|
|
variableModes: { col1: 'light' }
|
|
})
|
|
|
|
expect(graph.resolveColorVariableForNode(child.id, 'v1')).toEqual({
|
|
r: 0,
|
|
g: 0,
|
|
b: 0,
|
|
a: 1
|
|
})
|
|
expect(graph.resolveColorVariableForNode(nestedOverride.id, 'v1')).toEqual({
|
|
r: 1,
|
|
g: 1,
|
|
b: 1,
|
|
a: 1
|
|
})
|
|
})
|
|
|
|
test('missing active mode falls back to default value', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Theme',
|
|
modes: [
|
|
{ modeId: 'light', name: 'Light' },
|
|
{ modeId: 'dark', name: 'Dark' }
|
|
],
|
|
defaultModeId: 'light',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v1',
|
|
name: 'bg',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { light: { r: 1, g: 1, b: 1, a: 1 } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
graph.setActiveMode('col1', 'dark')
|
|
expect(graph.resolveColorVariable('v1')).toEqual({ r: 1, g: 1, b: 1, a: 1 })
|
|
})
|
|
|
|
test('explicit alias resolution preserves source mode', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Theme',
|
|
modes: [
|
|
{ modeId: 'light', name: 'Light' },
|
|
{ modeId: 'dark', name: 'Dark' }
|
|
],
|
|
defaultModeId: 'light',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'base',
|
|
name: 'base',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: {
|
|
light: { r: 1, g: 1, b: 1, a: 1 },
|
|
dark: { r: 0, g: 0, b: 0, a: 1 }
|
|
},
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
graph.addVariable({
|
|
id: 'alias',
|
|
name: 'alias',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: {
|
|
light: { aliasId: 'base' },
|
|
dark: { aliasId: 'base' }
|
|
},
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
expect(graph.resolveVariable('alias', 'dark')).toEqual({ r: 0, g: 0, b: 0, a: 1 })
|
|
})
|
|
|
|
test('bind and unbind variable to node', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Colors',
|
|
modes: [{ modeId: 'm1', name: 'Light' }],
|
|
defaultModeId: 'm1',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v1',
|
|
name: 'Primary',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: { r: 1, g: 0, b: 0, a: 1 } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
name: 'Rect',
|
|
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, visible: true, opacity: 1 }]
|
|
})
|
|
|
|
graph.bindVariable(node.id, 'fills/0/color', 'v1')
|
|
expect(node.boundVariables['fills/0/color']).toBe('v1')
|
|
|
|
graph.unbindVariable(node.id, 'fills/0/color')
|
|
expect(node.boundVariables['fills/0/color']).toBeUndefined()
|
|
})
|
|
|
|
test('removing variable cleans up bindings', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Colors',
|
|
modes: [{ modeId: 'm1', name: 'Default' }],
|
|
defaultModeId: 'm1',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v1',
|
|
name: 'Red',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: { r: 1, g: 0, b: 0, a: 1 } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
name: 'Rect',
|
|
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, visible: true, opacity: 1 }]
|
|
})
|
|
graph.bindVariable(node.id, 'fills/0/color', 'v1')
|
|
expect(node.boundVariables['fills/0/color']).toBe('v1')
|
|
|
|
graph.removeVariable('v1')
|
|
expect(node.boundVariables['fills/0/color']).toBeUndefined()
|
|
expect(graph.variables.size).toBe(0)
|
|
})
|
|
|
|
test('circular alias does not infinite loop', () => {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Test',
|
|
modes: [{ modeId: 'm1', name: 'Default' }],
|
|
defaultModeId: 'm1',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v1',
|
|
name: 'A',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: { aliasId: 'v2' } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
graph.addVariable({
|
|
id: 'v2',
|
|
name: 'B',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: { aliasId: 'v1' } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
|
|
expect(graph.resolveColorVariable('v1')).toBeUndefined()
|
|
})
|
|
})
|