import { describe, expect, it } from 'vitest'; import { AddVariableCommand } from '../../src/core/commands/variableCommands'; import { GraphInterpreter } from '../../src/core/graph'; import type { BehaviorAsset } from '../../src/core/model/Asset'; import { behaviorLookup } from '../../src/core/model/behaviorPrograms'; import { findAsset } from '../../src/core/model/Project'; import { deserializeProject, serializeProject } from '../../src/core/store/serialize'; import { fakeContext, makeBehavior, makeProject } from '../helpers'; type RawNode = { id: string; type: string; params?: Record; ports?: unknown[] }; type RawEdge = [from: string, fromPort: string, to: string, toPort: string]; /** A saved project whose behavior holds `nodes` and `edges` exactly as an older editor wrote them. */ function loadOld(nodes: RawNode[], edges: RawEdge[], variables = ['px', 'py', 'sum']) { const fixture = makeProject(); const behavior = makeBehavior(fixture, 'B', variables); fixture.stack.execute(new AddVariableCommand(behavior.id, 'piece', 'struct')); fixture.stack.execute(new AddVariableCommand(behavior.id, 'flag', 'boolean')); const raw = serializeProject(fixture.project) as { assets: Record[] }; raw.assets.find((a) => a.id === behavior.id)!.graph = { nodes: nodes.map((n) => ({ x: 0, y: 0, params: {}, ...n })), edges: edges.map(([from, fromPort, to, toPort], i) => ({ id: `e${i}`, from: { node: from, port: fromPort }, to: { node: to, port: toPort }, })), }; const project = deserializeProject(raw); return { project, behavior: findAsset(project, behavior.id) as BehaviorAsset }; } const node = (behavior: BehaviorAsset, id: string) => behavior.graph.nodes.find((n) => n.id === id)!; const edges = (behavior: BehaviorAsset) => behavior.graph.edges.map((e) => `${e.from.node}.${e.from.port}>${e.to.node}.${e.to.port}`); describe('variable nodes of an older file', () => { it('load as Variables nodes that compute what the old graph computed', () => { const { project, behavior } = loadOld( [ { id: 'start', type: 'OnStart' }, { id: 'set', type: 'SetVariables', params: { px: 7 }, ports: [{ key: 'px', kind: 'number' }] }, { id: 'y', type: 'SetVariable', params: { variable: 'py', value: 9 } }, { id: 'get', type: 'GetVariable', params: { variable: 'px' } }, { id: 'both', type: 'GetVariables', ports: [{ key: 'py', kind: 'number' }] }, { id: 'add', type: 'Add' }, { id: 'sum', type: 'Variable', params: { variable: 'sum' } }, ], [ ['start', 'exec', 'set', 'exec'], ['set', 'exec', 'y', 'exec'], ['y', 'exec', 'sum', 'exec'], ['get', 'value', 'add', 'a'], ['both', 'py', 'add', 'b'], ['add', 'result', 'sum', 'value'], ], ); expect(behavior.graph.nodes.map((n) => n.type)).toEqual([ 'OnStart', 'Variables', 'Variables', 'Variables', 'Variables', 'Add', 'Variables', ]); expect(edges(behavior)).toEqual([ 'start.exec>set.exec', 'set.exec>y.exec', 'y.exec>sum.exec', 'get.px>add.a', 'both.py>add.b', 'add.result>sum.sum', ]); const ctx = fakeContext({ variables: { px: 0, py: 0, sum: 0 } }); new GraphInterpreter(behavior.graph, behaviorLookup(project)).fire('OnStart', ctx); expect(ctx.variables).toEqual({ px: 7, py: 9, sum: 16 }); }); it('keep whether they wrote, their typed-in value, and the kind of the variable', () => { const { behavior } = loadOld( [ { id: 'get', type: 'GetVariable', params: { variable: 'flag' } }, { id: 'set', type: 'SetVariable', params: { variable: 'px', value: 3 } }, { id: 'plural', type: 'SetVariables', params: { py: 4 }, ports: [{ key: 'py', kind: 'number' }] }, { id: 'gone', type: 'Variable', params: { variable: 'nope' } }, ], [], ); expect(node(behavior, 'get').ports).toEqual([{ key: 'flag', kind: 'boolean' }]); expect(node(behavior, 'set')).toMatchObject({ params: { px: 3 }, ports: [{ key: 'px', kind: 'number', write: true }], }); expect(node(behavior, 'plural')).toMatchObject({ params: { py: 4 }, ports: [{ key: 'py', kind: 'number', write: true }], }); // A variable the behavior does not declare keeps its port, as text; the inspector marks it. expect(node(behavior, 'gone').ports).toEqual([{ key: 'nope', kind: 'text', write: true }]); expect(node(behavior, 'get').params).toEqual({}); }); it('give a struct variable a struct port, so an Expand can still take it apart', () => { const { behavior } = loadOld([{ id: 'get', type: 'GetVariable', params: { variable: 'piece' } }], []); expect(node(behavior, 'get').ports).toEqual([{ key: 'piece', kind: 'struct' }]); }); it('drop every edge of a node that never had a variable picked, flow included', () => { const { behavior } = loadOld( [ { id: 'start', type: 'OnStart' }, { id: 'empty', type: 'SetVariable' }, { id: 'add', type: 'Add' }, ], [ ['start', 'exec', 'empty', 'exec'], ['add', 'result', 'empty', 'value'], ], ); expect(node(behavior, 'empty').ports).toBeUndefined(); expect(edges(behavior)).toEqual([]); }); it('are left alone once they are Variables nodes', () => { const { project, behavior } = loadOld( [{ id: 'get', type: 'GetVariable', params: { variable: 'px' } }], [], ); const again = findAsset(deserializeProject(serializeProject(project)), behavior.id) as BehaviorAsset; expect(again.graph).toEqual(behavior.graph); }); });