229 lines
7.1 KiB
TypeScript
229 lines
7.1 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { SceneGraph } from '@open-pencil/core'
|
|
import noHardcodedColors from '@open-pencil/core/lint/rules/no-hardcoded-colors'
|
|
|
|
function pageId(graph: SceneGraph): string {
|
|
return graph.getPages()[0].id
|
|
}
|
|
|
|
function setupColorVars(graph: SceneGraph, ...ids: string[]): void {
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Colors',
|
|
modes: [{ modeId: 'm1', name: 'Light' }],
|
|
defaultModeId: 'm1',
|
|
variableIds: []
|
|
})
|
|
for (const id of ids) {
|
|
graph.addVariable({
|
|
id,
|
|
name: `Var ${id}`,
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: { r: 0.5, g: 0.5, b: 0.5, a: 1 } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─── bindVariable emits node:updated event ──────────────────────────────────
|
|
|
|
describe('bindVariable emits node:updated event', () => {
|
|
test('bindVariable emits node:updated event', () => {
|
|
const graph = new SceneGraph()
|
|
setupColorVars(graph, 'v1')
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
name: 'Rect',
|
|
fills: [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5, a: 1 }, visible: true, opacity: 1 }]
|
|
})
|
|
|
|
const events: Array<{ nodeId: string; changes: Record<string, unknown> }> = []
|
|
graph.onNodeEvents({
|
|
updated: (nodeId, changes) => {
|
|
events.push({ nodeId, changes })
|
|
}
|
|
})
|
|
|
|
graph.bindVariable(node.id, 'fills/0/color', 'v1')
|
|
|
|
expect(events.length).toBeGreaterThanOrEqual(1)
|
|
expect(events[0].nodeId).toBe(node.id)
|
|
expect(events[0].changes).toHaveProperty('boundVariables')
|
|
})
|
|
})
|
|
|
|
// ─── no-hardcoded-colors lint checks indexed bindings ──────────────────────
|
|
|
|
describe('no-hardcoded-colors lint checks indexed bindings', () => {
|
|
test('node with fills/0/color binding passes lint (no false positive)', () => {
|
|
const node = {
|
|
id: 'n1',
|
|
name: 'Rect',
|
|
type: 'RECTANGLE',
|
|
width: 100,
|
|
height: 100,
|
|
x: 0,
|
|
y: 0,
|
|
rotation: 0,
|
|
visible: true,
|
|
locked: false,
|
|
layoutMode: 'NONE',
|
|
itemSpacing: 0,
|
|
paddingTop: 0,
|
|
paddingRight: 0,
|
|
paddingBottom: 0,
|
|
paddingLeft: 0,
|
|
cornerRadius: 0,
|
|
childIds: [],
|
|
text: '',
|
|
fontSize: 14,
|
|
styleRunCount: 0,
|
|
boundVariables: { 'fills/0/color': 'v1' },
|
|
fills: [{ type: 'SOLID', visible: true, opacity: 1, color: { r: 0.5, g: 0.5, b: 0.5 } }],
|
|
strokes: [],
|
|
effects: []
|
|
}
|
|
|
|
const messages: Array<{ message: string }> = []
|
|
const context = {
|
|
report: (issue: { message: string }) => messages.push(issue),
|
|
getConfig: () => ({}),
|
|
getParent: () => null,
|
|
getChildren: () => []
|
|
}
|
|
|
|
noHardcodedColors.check(node, context)
|
|
// Should NOT report a violation — the color is bound to a variable
|
|
expect(messages.length).toBe(0)
|
|
})
|
|
|
|
test('node with top-level fills binding still reports violation (no false negative)', () => {
|
|
const node = {
|
|
id: 'n2',
|
|
name: 'Rect',
|
|
type: 'RECTANGLE',
|
|
width: 100,
|
|
height: 100,
|
|
x: 0,
|
|
y: 0,
|
|
rotation: 0,
|
|
visible: true,
|
|
locked: false,
|
|
layoutMode: 'NONE',
|
|
itemSpacing: 0,
|
|
paddingTop: 0,
|
|
paddingRight: 0,
|
|
paddingBottom: 0,
|
|
paddingLeft: 0,
|
|
cornerRadius: 0,
|
|
childIds: [],
|
|
text: '',
|
|
fontSize: 14,
|
|
styleRunCount: 0,
|
|
boundVariables: { fills: 'v1' }, // top-level — renderer-ignored
|
|
fills: [{ type: 'SOLID', visible: true, opacity: 1, color: { r: 0.5, g: 0.5, b: 0.5 } }],
|
|
strokes: [],
|
|
effects: []
|
|
}
|
|
|
|
const messages: Array<{ message: string }> = []
|
|
const context = {
|
|
report: (issue: { message: string }) => messages.push(issue),
|
|
getConfig: () => ({}),
|
|
getParent: () => null,
|
|
getChildren: () => []
|
|
}
|
|
|
|
noHardcodedColors.check(node, context)
|
|
// SHOULD report a violation — top-level 'fills' binding is renderer-ignored
|
|
expect(messages.length).toBeGreaterThanOrEqual(1)
|
|
})
|
|
})
|
|
|
|
// ─── bindVariable validation ──────────────────────────────────────────────
|
|
|
|
describe('bindVariable validation', () => {
|
|
function setupGraph(): SceneGraph {
|
|
const graph = new SceneGraph()
|
|
graph.addCollection({
|
|
id: 'col1',
|
|
name: 'Colors',
|
|
modes: [{ modeId: 'm1', name: 'Light' }],
|
|
defaultModeId: 'm1',
|
|
variableIds: []
|
|
})
|
|
graph.addVariable({
|
|
id: 'v-color',
|
|
name: 'Primary',
|
|
type: 'COLOR',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: { r: 1, g: 0, b: 0, a: 1 } },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
graph.addVariable({
|
|
id: 'v-float',
|
|
name: 'Spacing',
|
|
type: 'FLOAT',
|
|
collectionId: 'col1',
|
|
valuesByMode: { m1: 16 },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
return graph
|
|
}
|
|
|
|
test('bindVariable rejects nonexistent variable', () => {
|
|
const graph = setupGraph()
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), { name: 'Rect' })
|
|
expect(() => {
|
|
graph.bindVariable(node.id, 'fills/0/color', 'nonexistent')
|
|
}).toThrow()
|
|
expect(graph.getNode(node.id).boundVariables['fills/0/color']).toBeUndefined()
|
|
})
|
|
|
|
test('bindVariable rejects FLOAT variable on color field', () => {
|
|
const graph = setupGraph()
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), { name: 'Rect' })
|
|
expect(() => {
|
|
graph.bindVariable(node.id, 'fills/0/color', 'v-float')
|
|
}).toThrow()
|
|
expect(graph.getNode(node.id).boundVariables['fills/0/color']).toBeUndefined()
|
|
})
|
|
|
|
test('bindVariable rejects COLOR variable on scalar field', () => {
|
|
const graph = setupGraph()
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), { name: 'Rect' })
|
|
expect(() => {
|
|
graph.bindVariable(node.id, 'opacity', 'v-color')
|
|
}).toThrow()
|
|
expect(graph.getNode(node.id).boundVariables['opacity']).toBeUndefined()
|
|
})
|
|
|
|
test('bindVariable rejects binding to fills that do not yet exist', () => {
|
|
const graph = setupGraph()
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), { name: 'Rect' })
|
|
// Rect has 0 fills — binding to fills/0/color must be rejected (index >= length)
|
|
expect(() => {
|
|
graph.bindVariable(node.id, 'fills/0/color', 'v-color')
|
|
}).toThrow(/out of range/)
|
|
})
|
|
|
|
test('bindVariable auto-removes top-level fills binding when indexed binding is set', () => {
|
|
const graph = setupGraph()
|
|
const node = graph.createNode('RECTANGLE', pageId(graph), {
|
|
name: 'Rect',
|
|
fills: [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5, a: 1 }, visible: true, opacity: 1 }]
|
|
})
|
|
const n = graph.getNode(node.id)
|
|
// Set top-level dead data first
|
|
n.boundVariables['fills'] = 'v-color'
|
|
// Now set a proper indexed binding — should auto-remove top-level
|
|
graph.bindVariable(node.id, 'fills/0/color', 'v-color')
|
|
expect(n.boundVariables['fills']).toBeUndefined()
|
|
expect(n.boundVariables['fills/0/color']).toBe('v-color')
|
|
})
|
|
})
|