import { describe, expect, it } from 'vitest'; import { GraphInterpreter, inlineInputDef, inlineInputs, inlineValue, type PortDef, } from '../../src/core/graph'; import { BUILT_IN_NODES } from '../../src/core/graph/nodes'; import { graphLookup } from '../../src/core/model/graphLookup'; import { fakeContext, graphEditor, makeBehavior, makeProject } from '../helpers'; const untouched = { params: {} }; /** What a flow logs when `build` hangs a value in front of `To text` → `Log`. */ function logged( build: ( edit: ReturnType, ) => [ReturnType['addNode']>, string], ) { const fixture = makeProject(); const behavior = makeBehavior(fixture, 'B', []); const edit = graphEditor(fixture, behavior); const start = edit.addNode('OnStart'); const log = edit.addNode('Log'); const text = edit.addNode('ToText'); const [source, port] = build(edit); edit.wire([ [start, 'exec', log, 'exec'], [text, 'text', log, 'message'], ...(source === text ? [] : ([[source, port, text, 'value']] as const)), ]); const ctx = fakeContext(); new GraphInterpreter(behavior.graph, graphLookup(fixture.project)).fire('OnStart', ctx); return ctx.logs; } describe('the value of an unconnected input', () => { it('is, on every built-in node, the default the inspector shows – never null', () => { const wrong: string[] = []; for (const def of BUILT_IN_NODES) { for (const port of inlineInputs(def)) { const value = inlineValue(untouched, port); const shown = inlineInputDef(port).default; if (value === null || JSON.stringify(value) !== JSON.stringify(shown)) wrong.push(`${def.type}.${port.key}: ${JSON.stringify(value)} vs ${JSON.stringify(shown)}`); } } expect(wrong).toEqual([]); }); it('is the zero of its kind: false, 0, "" and an empty list', () => { const port = (type: PortDef['type']): PortDef => ({ key: 'p', label: 'p', type }); expect( ['boolean', 'number', 'string', 'any', 'list'].map((t) => inlineValue(untouched, port(t as PortDef['type'])), ), ).toEqual([false, 0, '', '', []]); }); it('stays null where nothing can be typed in: an object is a live handle', () => { expect(inlineValue(untouched, { key: 'o', label: 'o', type: 'object' })).toBeNull(); expect(inlineValue(untouched, { key: 'n', label: 'n', type: 'number', inline: false })).toBeNull(); }); it('takes the node’s own value first, then the port default', () => { const port: PortDef = { key: 'n', label: 'n', type: 'number', default: 5 }; expect(inlineValue(untouched, port)).toBe(5); expect(inlineValue({ params: { n: 7 } }, port)).toBe(7); }); it('hands out a fresh empty list each time, so editing one in place touches no other', () => { const port: PortDef = { key: 'l', label: 'l', type: 'list' }; const first = inlineValue(untouched, port) as unknown[]; first.push(1); expect(inlineValue(untouched, port)).toEqual([]); }); }); describe('what a flow computes from an untouched node', () => { it('turns empty text into "", not "0"', () => { expect(logged((edit) => [edit.addNode('ToText'), 'text'])).toEqual(['']); }); it('lets Select and First not empty pass on "" instead of nothing', () => { expect(logged((edit) => [edit.addNode('Select'), 'value'])).toEqual(['']); expect(logged((edit) => [edit.addNode('FirstNotEmpty'), 'value'])).toEqual(['']); }); it('still reads a typed-in number as that number', () => { expect(logged((edit) => [edit.addNode('Number', { value: 3 }), 'value'])).toEqual(['3']); }); });