153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import {
|
|
createLibraryRevision,
|
|
deserializeLibraryRevision,
|
|
diffLibraryManifests,
|
|
ensureLibraryAssetKeys,
|
|
extractLibrarySnapshot,
|
|
MemoryLibraryCatalog,
|
|
serializeLibraryRevision
|
|
} from '@open-pencil/core/library'
|
|
import { SceneGraph } from '@open-pencil/scene-graph'
|
|
|
|
function setupLibraryGraph() {
|
|
const graph = new SceneGraph()
|
|
const page = graph.getPages()[0]
|
|
if (!page) throw new Error('Expected page')
|
|
const icon = graph.createNode('COMPONENT', page.id, {
|
|
name: 'Icon',
|
|
componentKey: 'icon'
|
|
})
|
|
graph.createNode('ELLIPSE', icon.id, { name: 'Glyph' })
|
|
const button = graph.createNode('COMPONENT', page.id, {
|
|
name: 'Button',
|
|
componentKey: 'button'
|
|
})
|
|
const nestedIcon = graph.createInstance(icon.id, button.id)
|
|
if (!nestedIcon) throw new Error('Expected nested instance')
|
|
return { graph, icon, button }
|
|
}
|
|
|
|
describe('component library snapshots', () => {
|
|
test('extracts selected assets with nested component dependencies and remapped IDs', () => {
|
|
const { graph, icon, button } = setupLibraryGraph()
|
|
const snapshot = extractLibrarySnapshot(graph, [button.id])
|
|
expect(snapshot.assetRoots.map((node) => node.name)).toEqual(['Button'])
|
|
const components = [...snapshot.graph.getAllNodes()].filter((node) => node.type === 'COMPONENT')
|
|
expect(components.map((node) => node.name).sort()).toEqual(['Button', 'Icon'])
|
|
const importedButton = snapshot.assetRoots[0]
|
|
const importedNested = importedButton
|
|
? snapshot.graph.getChildren(importedButton.id).find((node) => node.type === 'INSTANCE')
|
|
: undefined
|
|
expect(importedNested?.componentId).not.toBe(icon.id)
|
|
expect(snapshot.graph.getNode(importedNested?.componentId ?? '')?.name).toBe('Icon')
|
|
})
|
|
|
|
test('creates deterministic content revisions independent of local node IDs', async () => {
|
|
const first = setupLibraryGraph()
|
|
const second = setupLibraryGraph()
|
|
const inputs = {
|
|
libraryId: 'design-system',
|
|
name: 'Design system',
|
|
publishedAt: '2026-01-01T00:00:00.000Z'
|
|
}
|
|
const firstRevision = await createLibraryRevision({ ...inputs, graph: first.graph })
|
|
const secondRevision = await createLibraryRevision({ ...inputs, graph: second.graph })
|
|
expect(firstRevision.manifest.revisionId).toBe(secondRevision.manifest.revisionId)
|
|
expect(firstRevision.manifest.assets.map((asset) => asset.key).sort()).toEqual([
|
|
'button',
|
|
'icon'
|
|
])
|
|
|
|
first.graph.updateNode(first.button.id, { opacity: 0.5 })
|
|
const changedRevision = await createLibraryRevision({ ...inputs, graph: first.graph })
|
|
expect(changedRevision.manifest.revisionId).not.toBe(firstRevision.manifest.revisionId)
|
|
})
|
|
|
|
test('diffs revisions by stable asset key', async () => {
|
|
const { graph, button, icon } = setupLibraryGraph()
|
|
const base = await createLibraryRevision({
|
|
libraryId: 'design-system',
|
|
name: 'Design system',
|
|
graph,
|
|
publishedAt: '2026-01-01T00:00:00.000Z'
|
|
})
|
|
graph.updateNode(button.id, { opacity: 0.5 })
|
|
graph.updateNode(icon.id, { name: 'System icon' })
|
|
const next = await createLibraryRevision({
|
|
libraryId: 'design-system',
|
|
name: 'Design system',
|
|
graph,
|
|
previousRevisionId: base.manifest.revisionId,
|
|
publishedAt: '2026-01-02T00:00:00.000Z'
|
|
})
|
|
expect(diffLibraryManifests(base.manifest, next.manifest).map((change) => change.kind)).toEqual(
|
|
['renamed', 'modified']
|
|
)
|
|
})
|
|
|
|
test('assigns durable unique keys once and preserves them across rename', () => {
|
|
const { graph, button, icon } = setupLibraryGraph()
|
|
graph.updateNode(button.id, { componentKey: null })
|
|
graph.updateNode(icon.id, { componentKey: null })
|
|
expect(
|
|
ensureLibraryAssetKeys(graph)
|
|
.map((assignment) => assignment.assetKey)
|
|
.sort()
|
|
).toEqual(['button', 'icon'])
|
|
graph.updateNode(button.id, { name: 'Primary action' })
|
|
expect(
|
|
ensureLibraryAssetKeys(graph).find((assignment) => assignment.nodeId === button.id)
|
|
).toEqual({
|
|
nodeId: button.id,
|
|
assetKey: 'button',
|
|
existing: true
|
|
})
|
|
})
|
|
|
|
test('serializes portable revisions without losing component links', async () => {
|
|
const { graph } = setupLibraryGraph()
|
|
const revision = await createLibraryRevision({
|
|
libraryId: 'design-system',
|
|
name: 'Design system',
|
|
graph
|
|
})
|
|
const restored = deserializeLibraryRevision(structuredClone(serializeLibraryRevision(revision)))
|
|
expect(restored.manifest).toEqual(revision.manifest)
|
|
const button = [...restored.graph.getAllNodes()].find((node) => node.componentKey === 'button')
|
|
const nested = button
|
|
? restored.graph.getChildren(button.id).find((node) => node.type === 'INSTANCE')
|
|
: undefined
|
|
expect(restored.graph.getNode(nested?.componentId ?? '')?.componentKey).toBe('icon')
|
|
})
|
|
|
|
test('publishes immutable revisions with optimistic conflict detection', async () => {
|
|
const { graph } = setupLibraryGraph()
|
|
const catalog = new MemoryLibraryCatalog()
|
|
const first = await catalog.publishRevision({
|
|
libraryId: 'design-system',
|
|
name: 'Design system',
|
|
graph,
|
|
publishedAt: '2026-01-01T00:00:00.000Z'
|
|
})
|
|
expect(await catalog.listLibraries()).toEqual([
|
|
{
|
|
libraryId: 'design-system',
|
|
name: 'Design system',
|
|
latestRevisionId: first.manifest.revisionId,
|
|
publishedAt: '2026-01-01T00:00:00.000Z',
|
|
assetCount: 2
|
|
}
|
|
])
|
|
await expect(
|
|
catalog.publishRevision({
|
|
libraryId: 'design-system',
|
|
name: 'Design system',
|
|
graph,
|
|
previousRevisionId: 'stale'
|
|
})
|
|
).rejects.toThrow('latest revision has changed')
|
|
})
|
|
})
|