openpencil/packages/scene-graph/tests/components/property-driven-sync.test.ts
Danila Poyarkov aeba9a6bd5
fix(scene-graph): leave a field a component property drives to its instance (#843)
* fix(scene-graph): leave a field a component property drives to its instance

A component states the default for a field a property reference drives,
but the enclosing instance's assignment decides the value. Synchronising
copied the default over it, so loading a page into an open document —
which synchronises every component it places — reset those layers. A
label a navigation item assigned came back as the component's default
when its page loaded on its own, while reading the whole archive gave
the assigned text.

Synchronising now skips a field whose reference names a property an
enclosing instance assigns, and carries everything else as before.

* fix(scene-graph): read an own assignment, not an inherited key

A document-authored component property id can collide with an Object
prototype key, and `in` would then report an assignment the instance
never made, silently leaving the referenced field unsynchronised.

* test(core): cover visibility and swap on an independent page load

The fix skips any field a component property reference drives, so the
page-scoped test should exercise VISIBLE and INSTANCE_SWAP alongside
TEXT rather than leaving the other two to full-document reads.

* fix(scene-graph): leave slot content to its own guard

#850 added a SLOT_CONTENT reference field, which drives a slot frame's
children rather than a scalar field, so the property-driven field map
no longer claims to cover every reference field.
2026-10-04 01:38:23 +04:00

85 lines
3.2 KiB
TypeScript

import { expect, test } from 'bun:test'
import { SceneGraph } from '@open-pencil/scene-graph'
function graphWithAssignedLabel() {
const graph = new SceneGraph()
const page = graph.getPages()[0]
const component = graph.createNode('COMPONENT', page.id, {
name: 'NavItem',
componentPropertyDefinitions: [
{ id: '207:1', name: 'Label', type: 'TEXT', defaultValue: 'Default' }
]
})
const label = graph.createNode('TEXT', component.id, {
name: 'Label',
text: 'Default',
componentPropertyReferences: [{ propertyId: '207:1', field: 'TEXT' }]
})
const instance = graph.createInstance(component.id, page.id)
return { graph, component, label, instance }
}
/**
* A component states the default for a field a property drives; the enclosing instance's
* assignment decides the value. Synchronising must not copy the default over it.
*/
test('synchronizing leaves a field the enclosing instance assigns alone', () => {
const { graph, component, instance } = graphWithAssignedLabel()
const clone = graph.getChildren(instance.id)[0]
graph.updateNode(instance.id, { componentPropertyAssignments: { '207:1': 'Assigned' } })
graph.updateNode(clone.id, { text: 'Assigned' })
graph.syncInstances(component.id)
expect(graph.getNode(clone.id)?.text).toBe('Assigned')
})
test('synchronizing still carries the default to an instance that assigns nothing', () => {
const { graph, component, label, instance } = graphWithAssignedLabel()
const clone = graph.getChildren(instance.id)[0]
graph.updateNode(label.id, { text: 'Edited default' })
graph.syncInstances(component.id)
expect(graph.getNode(clone.id)?.text).toBe('Edited default')
})
/** Only the referenced field is left alone; the rest of the component edit still lands. */
test('synchronizing carries other fields of a property-driven layer', () => {
const { graph, component, label, instance } = graphWithAssignedLabel()
const clone = graph.getChildren(instance.id)[0]
graph.updateNode(instance.id, { componentPropertyAssignments: { '207:1': 'Assigned' } })
graph.updateNode(clone.id, { text: 'Assigned' })
graph.updateNode(label.id, { opacity: 0.5 })
graph.syncInstances(component.id)
expect(graph.getNode(clone.id)?.text).toBe('Assigned')
expect(graph.getNode(clone.id)?.opacity).toBe(0.5)
})
/** Property IDs are document-authored strings, so one may collide with an Object prototype key. */
test('a property id shared with an Object prototype key is not read as an assignment', () => {
const graph = new SceneGraph()
const page = graph.getPages()[0]
const component = graph.createNode('COMPONENT', page.id, {
name: 'NavItem',
componentPropertyDefinitions: [
{ id: 'toString', name: 'Label', type: 'TEXT', defaultValue: 'Default' }
]
})
const label = graph.createNode('TEXT', component.id, {
name: 'Label',
text: 'Default',
componentPropertyReferences: [{ propertyId: 'toString', field: 'TEXT' }]
})
const instance = graph.createInstance(component.id, page.id)
const clone = graph.getChildren(instance.id)[0]
graph.updateNode(label.id, { text: 'Edited default' })
graph.syncInstances(component.id)
expect(graph.getNode(clone.id)?.text).toBe('Edited default')
})