A new node handed null to every unconnected input that had no port default, even a Bool. The inspector showed false, 0, "" or an empty list at the same time, so what the node read was not what the editor said. 97 of 135 inputs of the built-in nodes were affected. - inlineValue() falls back to the default the inspector shows. One place covers every node, also future ones and saved projects. - A default list is handed out as a fresh copy per read, so a node that edits its input in place cannot change the shared default. - The node shows the value it reads: an empty text or list as (empty), an object or struct input without a link as (null). Live values while playing use the same words. - A key input without a key keeps showing a dash, like the key field in the inspector. - Visible changes in old projects: To text without input gives "" instead of "0", Select and First not empty give "" instead of null. Tests: - 8 new unit tests in tests/core/inline-defaults.test.ts, among them one over all built-in nodes that no typed-in input reads null. - The e2e check in module 6 reads the rows of an unlinked Name of node. The check sum stays the same. - ./check.sh: 167 files, 2434 tests, exit 0. e2e 6: 117 checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
3.6 KiB
TypeScript
94 lines
3.6 KiB
TypeScript
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<typeof graphEditor>,
|
||
) => [ReturnType<ReturnType<typeof graphEditor>['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']);
|
||
});
|
||
});
|