openpencil/tests/engine/editor/components/variants.test.ts
2026-08-14 19:31:41 +03:00

218 lines
8.7 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { createEditor } from '@open-pencil/core/editor'
function setupVariants() {
const editor = createEditor()
const pageId = editor.state.currentPageId
const componentSet = editor.graph.createNode('COMPONENT_SET', pageId, {
name: 'Button',
componentPropertyDefinitions: [
{
id: 'variant:type',
name: 'Type',
type: 'VARIANT',
defaultValue: 'Primary',
variantOptions: ['Primary', 'Secondary']
},
{
id: 'variant:size',
name: 'Size',
type: 'VARIANT',
defaultValue: 'Small',
variantOptions: ['Small', 'Large']
}
]
})
const createVariant = (type: string, size: string, x: number, y: number) =>
editor.graph.createNode('COMPONENT', componentSet.id, {
name: `Type=${type}, Size=${size}`,
x,
y,
componentPropertyValues: { Type: type, Size: size }
})
const primarySmall = createVariant('Primary', 'Small', 0, 0)
const primaryLarge = createVariant('Primary', 'Large', 140, 0)
const secondarySmall = createVariant('Secondary', 'Small', 0, 140)
return { editor, componentSet, primarySmall, primaryLarge, secondarySmall }
}
describe('variant authoring', () => {
test('keeps sparse multidimensional transitions exact and reports unavailable combinations', () => {
const { editor, primarySmall, primaryLarge } = setupVariants()
const instance = editor.graph.createInstance(primarySmall.id, editor.state.currentPageId)
if (!instance) throw new Error('Expected instance')
expect(editor.getVariantOptionAvailability(instance.id, 'Size')).toEqual([
{ value: 'Small', available: true },
{ value: 'Large', available: true }
])
expect(
editor.setInstanceComponentProperty(instance.id, 'variant:size', 'Large')
).toBeUndefined()
expect(editor.graph.getNode(instance.id)?.componentId).toBe(primaryLarge.id)
expect(editor.getVariantOptionAvailability(instance.id, 'Type')).toEqual([
{ value: 'Primary', available: true },
{ value: 'Secondary', available: false }
])
expect(editor.switchInstanceVariant(instance.id, 'Type', 'Secondary')).toEqual({
kind: 'unavailable',
requested: { Type: 'Secondary', Size: 'Large' }
})
expect(editor.graph.getNode(instance.id)?.componentId).toBe(primaryLarge.id)
})
test('renames dimensions and values atomically through undo and redo', () => {
const { editor, componentSet, primarySmall } = setupVariants()
expect(editor.renamePropertyDefinition(componentSet.id, 'variant:size', 'Density')).toBe(true)
expect(editor.graph.getNode(primarySmall.id)?.componentPropertyValues).toEqual({
Type: 'Primary',
Density: 'Small'
})
expect(editor.graph.getNode(primarySmall.id)?.name).toBe('Type=Primary, Density=Small')
editor.undo.undo()
expect(editor.graph.getNode(primarySmall.id)?.componentPropertyValues).toEqual({
Type: 'Primary',
Size: 'Small'
})
expect(editor.graph.getNode(primarySmall.id)?.name).toBe('Type=Primary, Size=Small')
editor.undo.redo()
expect(editor.renameVariantValue(componentSet.id, 'variant:size', 'Small', 'Compact')).toBe(
true
)
expect(editor.graph.getNode(primarySmall.id)?.componentPropertyValues.Density).toBe('Compact')
expect(
editor.graph
.getNode(componentSet.id)
?.componentPropertyDefinitions.find((definition) => definition.id === 'variant:size')
?.variantOptions
).toEqual(['Compact', 'Large'])
editor.undo.undo()
expect(editor.graph.getNode(primarySmall.id)?.componentPropertyValues.Density).toBe('Small')
})
test('adds and removes dimensions with complete undo snapshots', () => {
const { editor, componentSet, primarySmall } = setupVariants()
const propertyId = editor.addPropertyDefinition(componentSet.id, 'State', 'VARIANT', 'Enabled')
expect(propertyId).toStartWith('prop:')
expect(editor.graph.getNode(primarySmall.id)?.componentPropertyValues.State).toBe('Enabled')
editor.undo.undo()
expect(editor.graph.getNode(primarySmall.id)?.componentPropertyValues).not.toHaveProperty(
'State'
)
editor.undo.redo()
expect(editor.graph.getNode(primarySmall.id)?.componentPropertyValues.State).toBe('Enabled')
if (!propertyId) throw new Error('Expected property ID')
expect(editor.removePropertyDefinition(componentSet.id, propertyId)).toBe(true)
expect(editor.graph.getNode(primarySmall.id)?.componentPropertyValues).not.toHaveProperty(
'State'
)
editor.undo.undo()
expect(editor.graph.getNode(primarySmall.id)?.componentPropertyValues.State).toBe('Enabled')
})
test('reorders properties and values atomically', () => {
const { editor, componentSet, primarySmall } = setupVariants()
expect(
editor.reorderPropertyDefinitions(componentSet.id, ['variant:size', 'variant:type'])
).toBe(true)
expect(
editor.graph
.getNode(componentSet.id)
?.componentPropertyDefinitions.map((definition) => definition.id)
).toEqual(['variant:size', 'variant:type'])
expect(editor.graph.getNode(primarySmall.id)?.name).toBe('Size=Small, Type=Primary')
expect(editor.reorderVariantValues(componentSet.id, 'variant:size', ['Large', 'Small'])).toBe(
true
)
expect(
editor.graph
.getNode(componentSet.id)
?.componentPropertyDefinitions.find((definition) => definition.id === 'variant:size')
).toMatchObject({ defaultValue: 'Large', variantOptions: ['Large', 'Small'] })
editor.undo.undo()
expect(
editor.graph
.getNode(componentSet.id)
?.componentPropertyDefinitions.find((definition) => definition.id === 'variant:size')
).toMatchObject({ defaultValue: 'Small', variantOptions: ['Small', 'Large'] })
editor.undo.undo()
expect(editor.graph.getNode(primarySmall.id)?.name).toBe('Type=Primary, Size=Small')
})
test('uses top-left defaults and validates missing or duplicate combinations', () => {
const { editor, componentSet, primarySmall, primaryLarge } = setupVariants()
const topLeft = editor.graph.getNode(primarySmall.id)
const topRight = editor.graph.getNode(primaryLarge.id)
if (!topLeft || !topRight) throw new Error('Expected variants')
editor.graph.updateNode(topLeft.id, { x: 200, y: 200 })
editor.graph.updateNode(topRight.id, { x: 0, y: 0 })
expect(editor.getDefaultVariantForComponentSet(componentSet.id)?.id).toBe(primaryLarge.id)
editor.graph.updateNode(primaryLarge.id, {
componentPropertyValues: { Type: 'Primary', Size: '' }
})
expect(editor.validateComponentSet(componentSet.id)).toEqual([
{
kind: 'missing-value',
propertyId: 'variant:size',
propertyName: 'Size',
componentIds: [primaryLarge.id]
}
])
editor.graph.updateNode(primaryLarge.id, {
componentPropertyValues: { Type: 'Primary', Size: 'Small' }
})
expect(editor.validateComponentSet(componentSet.id)).toEqual([
{
kind: 'duplicate-combination',
values: { Type: 'Primary', Size: 'Small' },
componentIds: [primarySmall.id, primaryLarge.id]
}
])
})
test('edits combinations, diagnoses conflicts, and duplicates or removes variants', () => {
const { editor, componentSet, primarySmall, primaryLarge } = setupVariants()
expect(editor.setVariantPropertyValue(primaryLarge.id, 'variant:size', 'Small')).toEqual({
kind: 'conflict',
componentIds: [primaryLarge.id, primarySmall.id]
})
expect(editor.getComponentSetVariantConflicts(componentSet.id)).toEqual([])
expect(editor.graph.getNode(primaryLarge.id)?.componentPropertyValues.Size).toBe('Large')
expect(editor.setVariantPropertyValue(primaryLarge.id, 'variant:size', 'Medium')).toEqual({
kind: 'changed'
})
expect(editor.graph.getNode(primaryLarge.id)?.componentPropertyValues.Size).toBe('Medium')
editor.undo.undo()
expect(editor.graph.getNode(primaryLarge.id)?.componentPropertyValues.Size).toBe('Large')
const duplicateId = editor.duplicateVariant(primarySmall.id)
expect(duplicateId).toBeString()
expect(editor.getComponentSetVariantConflicts(componentSet.id)).toHaveLength(1)
editor.undo.undo()
expect(duplicateId ? editor.graph.getNode(duplicateId) : undefined).toBeUndefined()
editor.undo.redo()
expect(duplicateId ? editor.graph.getNode(duplicateId)?.parentId : undefined).toBe(
componentSet.id
)
if (!duplicateId) throw new Error('Expected duplicate')
expect(editor.removeVariant(duplicateId)).toBe(true)
expect(editor.graph.getNode(duplicateId)).toBeUndefined()
editor.undo.undo()
expect(editor.graph.getNode(duplicateId)?.parentId).toBe(componentSet.id)
})
})