import { describe, expect, it } from 'vitest'; import { AddStructFieldCommand, AddVariableCommand, RemoveStructFieldCommand, RenameStructFieldCommand, } from '../../src/core/commands/variableCommands'; import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands'; import { findAsset, objectHolders, type Project } from '../../src/core/model/Project'; import { coerceValue, variableDef } from '../../src/core/properties/PropertySchema'; import { makeBehavior, makeSpriteScene, place, roundTrip } from '../helpers'; const findObject = (project: Project, id: string) => objectHolders(project) .flatMap((holder) => holder.objects) .find((o) => o.id === id); const structDef = (project: Project, assetId: string, key: string) => { const asset = findAsset(project, assetId); const def = asset?.type === 'behavior' ? asset.variables.find((v) => v.key === key) : undefined; return def?.kind === 'struct' ? def : undefined; }; /** A behavior with a struct variable `piece`, placed once with an override of its own. */ function withStruct() { const f = makeSpriteScene(); const behavior = makeBehavior(f, 'B'); f.stack.execute(new AddVariableCommand(behavior.id, 'piece', 'struct')); f.stack.execute(new AddStructFieldCommand(behavior.id, 'piece', { key: 'x', kind: 'number' })); const object = place(f, behavior.id, { x: 0, y: 0 }); f.stack.execute(new SetOverrideCommand(f.sceneId, object, 'piece', { x: 7 })); return { f, behaviorId: behavior.id, object }; } describe('a variable of type struct', () => { it('starts with no fields at all, so nothing is invented for it', () => { const f = makeSpriteScene(); const behavior = makeBehavior(f, 'B'); f.stack.execute(new AddVariableCommand(behavior.id, 'piece', 'struct')); expect(structDef(f.project, behavior.id, 'piece')?.fields).toEqual([]); expect(findAsset(f.project, behavior.id)?.props.piece).toEqual({}); }); it('grows a new field in the declaration, in its own value and in every override at once', () => { const { f, behaviorId, object } = withStruct(); f.stack.execute(new AddStructFieldCommand(behaviorId, 'piece', { key: 'name', kind: 'text' })); expect(structDef(f.project, behaviorId, 'piece')?.fields).toEqual([ { key: 'x', kind: 'number' }, { key: 'name', kind: 'text' }, ]); // The override deviates in `x` and must keep that while gaining the new field. expect(findObject(f.project, object)?.overrides.piece).toEqual({ x: 7, name: '' }); expect(findAsset(f.project, behaviorId)?.props.piece).toEqual({ x: 0, name: '' }); }); it('takes the field back out on undo, so no value keeps a shape the declaration dropped', () => { const { f, behaviorId, object } = withStruct(); f.stack.execute(new AddStructFieldCommand(behaviorId, 'piece', { key: 'name', kind: 'text' })); f.stack.undo(); expect(structDef(f.project, behaviorId, 'piece')?.fields).toEqual([{ key: 'x', kind: 'number' }]); expect(findAsset(f.project, behaviorId)?.props.piece).toEqual({ x: 0 }); expect(findObject(f.project, object)?.overrides.piece).toEqual({ x: 7 }); }); it('gives an override its own removed value back, not the default', () => { const { f, behaviorId, object } = withStruct(); f.stack.execute(new RemoveStructFieldCommand(behaviorId, 'piece', 'x')); expect(findObject(f.project, object)?.overrides.piece).toEqual({}); f.stack.undo(); // 7 is the override's own value; the behavior's default for the same field is 0. expect(findObject(f.project, object)?.overrides.piece).toEqual({ x: 7 }); expect(findAsset(f.project, behaviorId)?.props.piece).toEqual({ x: 0 }); }); it('carries the value along when a field is renamed, and back again on undo', () => { const { f, behaviorId, object } = withStruct(); f.stack.execute(new RenameStructFieldCommand(behaviorId, 'piece', 'x', 'column')); expect(structDef(f.project, behaviorId, 'piece')?.fields).toEqual([{ key: 'column', kind: 'number' }]); expect(findObject(f.project, object)?.overrides.piece).toEqual({ column: 7 }); f.stack.undo(); expect(findObject(f.project, object)?.overrides.piece).toEqual({ x: 7 }); }); it('refuses a second field of the same name', () => { const { f, behaviorId } = withStruct(); expect(() => f.stack.execute(new AddStructFieldCommand(behaviorId, 'piece', { key: 'x', kind: 'text' })), ).toThrow(/already exists/); }); it('survives saving and loading with its declaration, not just its value', () => { const { f, behaviorId, object } = withStruct(); f.stack.execute(new AddStructFieldCommand(behaviorId, 'piece', { key: 'name', kind: 'text' })); const loaded = roundTrip(f.project); expect(structDef(loaded, behaviorId, 'piece')?.fields).toEqual([ { key: 'x', kind: 'number' }, { key: 'name', kind: 'text' }, ]); expect(findObject(loaded, object)?.overrides.piece).toEqual({ x: 7, name: '' }); }); it('coerces a stored value to the shape its declaration promises', () => { const def = variableDef('piece', 'struct', undefined, [ { key: 'x', kind: 'number' }, { key: 'on', kind: 'boolean' }, ]); // An unknown key is dropped, a missing one starts empty, and '3' is a number here. expect(coerceValue(def, { x: '3', gone: 9 })).toEqual({ x: 3, on: false }); }); });