* test(core): move the fig round-trip suite into the package test home
The reader change added `verifier-contracts.test.ts` under
`tests/engine` and grew the migration baseline to admit it, which the
testing architecture forbids: new tests go to the canonical home and
the baseline only shrinks. It also started
`packages/core/tests/io/formats/fig/roundtrip/`, leaving two homes for
one domain.
These tests drive Core's exporter, so Core's home is the canonical one.
The directory moves whole — eleven files, its helpers and raw verifiers
with it — and picks up the local `assert`, `traversal`, `test-utils`
and fixture helpers already there. The baseline loses nine entries.
* refactor(core): address test helpers by alias instead of drilling
`open-pencil/no-deep-parent-relative-imports` already forbids `../../`
drilling, but `lint:structure` names the directories it checks and
`packages/{core,scene-graph,vue}/tests` were never added — so the 72
drilled imports in Core's tests, including the ones the round-trip move
just wrote, were never seen.
Core gains `#core-tests/*` beside fig's `#fig-tests/*`, registered with
the architecture rules, and its tests reach their helpers through it
and their source through `#core/*`. The three test directories join
`lint:structure`, which also surfaced six errors they had never been
checked for: four duplicated imports, an empty `noop`, and a
self-assignment the test meant, now written through a named binding.
Both guides state the rule, since a lint nobody can find in prose is
how this went unnoticed.
* fix(tools): point the heavy unit patterns at the relocated round-trip tests
`HEAVY_UNIT_TEST_PATTERNS` still listed the three heavy round-trip
tests under `tests/engine`. `test:unit` discovers them by directory so
the move looked clean, but `--heavy-only` selects by path and silently
stopped choosing them: 8 files, 52 tests, where it now runs 11 and 80.
263 lines
7.5 KiB
TypeScript
263 lines
7.5 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { SceneGraph, getInstanceOverride } from '@open-pencil/scene-graph'
|
|
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
|
|
})
|
|
}
|
|
}
|
|
|
|
function setupFloatVars(graph: SceneGraph, ...ids: string[]): void {
|
|
graph.addCollection({
|
|
id: 'col-f',
|
|
name: 'Floats',
|
|
modes: [{ modeId: 'm1', name: 'Light' }],
|
|
defaultModeId: 'm1',
|
|
variableIds: []
|
|
})
|
|
for (const id of ids) {
|
|
graph.addVariable({
|
|
id,
|
|
name: `Var ${id}`,
|
|
type: 'FLOAT',
|
|
collectionId: 'col-f',
|
|
valuesByMode: { m1: 1 },
|
|
description: '',
|
|
hiddenFromPublishing: false
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─── bindVariable on instance child sets override flag ──────────────────
|
|
|
|
describe('bindVariable on instance child sets override flag', () => {
|
|
test('binding on instance child sets boundVariables override on parent instance', () => {
|
|
const graph = new SceneGraph()
|
|
setupColorVars(graph, 'v1', 'v2')
|
|
const page = pageId(graph)
|
|
|
|
const component = graph.createNode('COMPONENT', page, {
|
|
name: 'Button',
|
|
width: 100,
|
|
height: 40
|
|
})
|
|
const child = graph.createNode('RECTANGLE', component.id, {
|
|
name: 'Bg',
|
|
width: 100,
|
|
height: 40,
|
|
fills: [{ type: 'SOLID', color: { r: 0.1, g: 0.1, b: 0.1, a: 1 }, visible: true, opacity: 1 }]
|
|
})
|
|
graph.bindVariable(child.id, 'fills/0/color', 'v1')
|
|
|
|
const instance =
|
|
graph.createInstance(component.id, page) ??
|
|
(() => {
|
|
throw new Error('instance failed')
|
|
})()
|
|
const instanceChild = graph.getChildren(instance.id)[0]
|
|
|
|
graph.bindVariable(instanceChild.id, 'fills/0/color', 'v2')
|
|
|
|
expect(
|
|
getInstanceOverride(
|
|
instance.instanceOverrides,
|
|
instance.id,
|
|
instanceChild.id,
|
|
'boundVariables'
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
test('binding on instance child survives syncInstances', () => {
|
|
const graph = new SceneGraph()
|
|
setupColorVars(graph, 'v1', 'v2')
|
|
const page = pageId(graph)
|
|
|
|
const component = graph.createNode('COMPONENT', page, {
|
|
name: 'Button',
|
|
width: 100,
|
|
height: 40
|
|
})
|
|
const child = graph.createNode('RECTANGLE', component.id, {
|
|
name: 'Bg',
|
|
width: 100,
|
|
height: 40,
|
|
fills: [{ type: 'SOLID', color: { r: 0.1, g: 0.1, b: 0.1, a: 1 }, visible: true, opacity: 1 }]
|
|
})
|
|
graph.bindVariable(child.id, 'fills/0/color', 'v1')
|
|
|
|
const instance =
|
|
graph.createInstance(component.id, page) ??
|
|
(() => {
|
|
throw new Error('instance failed')
|
|
})()
|
|
const instanceChild = graph.getChildren(instance.id)[0]
|
|
|
|
graph.bindVariable(instanceChild.id, 'fills/0/color', 'v2')
|
|
|
|
graph.syncInstances(component.id)
|
|
|
|
expect(instanceChild.boundVariables['fills/0/color']).toBe('v2')
|
|
})
|
|
})
|
|
|
|
// ─── bindVariable on INSTANCE node itself sets override ──────────────────
|
|
|
|
describe('bindVariable on INSTANCE node itself sets override', () => {
|
|
test('binding on INSTANCE node sets bare boundVariables override key', () => {
|
|
const graph = new SceneGraph()
|
|
setupFloatVars(graph, 'v1', 'v2')
|
|
const page = pageId(graph)
|
|
|
|
const component = graph.createNode('COMPONENT', page, {
|
|
name: 'Button',
|
|
width: 100,
|
|
height: 40,
|
|
opacity: 1
|
|
})
|
|
graph.bindVariable(component.id, 'opacity', 'v1')
|
|
|
|
const instance =
|
|
graph.createInstance(component.id, page) ??
|
|
(() => {
|
|
throw new Error('instance failed')
|
|
})()
|
|
|
|
graph.bindVariable(instance.id, 'opacity', 'v2')
|
|
|
|
expect(
|
|
getInstanceOverride(instance.instanceOverrides, instance.id, instance.id, 'boundVariables')
|
|
).toBe(true)
|
|
})
|
|
|
|
test('binding on INSTANCE node survives syncInstances', () => {
|
|
const graph = new SceneGraph()
|
|
setupFloatVars(graph, 'v1', 'v2')
|
|
const page = pageId(graph)
|
|
|
|
const component = graph.createNode('COMPONENT', page, {
|
|
name: 'Button',
|
|
width: 100,
|
|
height: 40,
|
|
opacity: 1
|
|
})
|
|
graph.bindVariable(component.id, 'opacity', 'v1')
|
|
|
|
const instance =
|
|
graph.createInstance(component.id, page) ??
|
|
(() => {
|
|
throw new Error('instance failed')
|
|
})()
|
|
|
|
graph.bindVariable(instance.id, 'opacity', 'v2')
|
|
|
|
graph.bindVariable(component.id, 'opacity', 'v1')
|
|
graph.syncInstances(component.id)
|
|
|
|
expect(instance.boundVariables['opacity']).toBe('v2')
|
|
})
|
|
})
|
|
|
|
// ─── removeVariable emits events and sets overrides ────────────────────
|
|
|
|
describe('removeVariable emits events and sets overrides', () => {
|
|
test('removeVariable emits node:updated for affected nodes', () => {
|
|
const graph = new SceneGraph()
|
|
setupColorVars(graph, 'v1')
|
|
const page = pageId(graph)
|
|
const node = graph.createNode('RECTANGLE', page, {
|
|
name: 'Rect',
|
|
fills: [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5, a: 1 }, visible: true, opacity: 1 }]
|
|
})
|
|
graph.bindVariable(node.id, 'fills/0/color', 'v1')
|
|
|
|
const events: Array<{ nodeId: string; changes: Record<string, unknown> }> = []
|
|
graph.onNodeEvents({
|
|
updated: (nodeId, changes) => events.push({ nodeId, changes })
|
|
})
|
|
|
|
graph.removeVariable('v1')
|
|
|
|
const updateEvent = events.find((e) => e.nodeId === node.id)
|
|
expect(updateEvent).toBeDefined()
|
|
if (updateEvent) {
|
|
expect('boundVariables' in updateEvent.changes).toBe(true)
|
|
}
|
|
})
|
|
|
|
test('removeVariable sets instance override on affected instance child', () => {
|
|
const graph = new SceneGraph()
|
|
setupColorVars(graph, 'v1')
|
|
const page = pageId(graph)
|
|
|
|
const component = graph.createNode('COMPONENT', page, {
|
|
name: 'Button',
|
|
width: 100,
|
|
height: 40
|
|
})
|
|
const compChild = graph.createNode('RECTANGLE', component.id, {
|
|
name: 'Bg',
|
|
width: 100,
|
|
height: 40,
|
|
fills: [{ type: 'SOLID', color: { r: 0.1, g: 0.1, b: 0.1, a: 1 }, visible: true, opacity: 1 }]
|
|
})
|
|
graph.bindVariable(compChild.id, 'fills/0/color', 'v1')
|
|
|
|
const instance =
|
|
graph.createInstance(component.id, page) ??
|
|
(() => {
|
|
throw new Error('instance failed')
|
|
})()
|
|
const instanceChild = graph.getChildren(instance.id)[0]
|
|
|
|
graph.removeVariable('v1')
|
|
|
|
expect(
|
|
getInstanceOverride(
|
|
instance.instanceOverrides,
|
|
instance.id,
|
|
instanceChild.id,
|
|
'boundVariables'
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
test('removeVariable does not emit for unaffected nodes', () => {
|
|
const graph = new SceneGraph()
|
|
setupFloatVars(graph, 'v1')
|
|
const page = pageId(graph)
|
|
const nodeA = graph.createNode('RECTANGLE', page, { name: 'A' })
|
|
const nodeB = graph.createNode('RECTANGLE', page, { name: 'B' })
|
|
graph.bindVariable(nodeA.id, 'opacity', 'v1')
|
|
|
|
const events: Array<{ nodeId: string }> = []
|
|
graph.onNodeEvents({
|
|
updated: (nodeId) => events.push({ nodeId })
|
|
})
|
|
|
|
graph.removeVariable('v1')
|
|
|
|
expect(events.some((e) => e.nodeId === nodeA.id)).toBe(true)
|
|
expect(events.some((e) => e.nodeId === nodeB.id)).toBe(false)
|
|
})
|
|
})
|