Get/Set variable nodes now title themselves "Get <name>" / "Set <name>", and
the new Variable node just "<name>"; the title is the same folded or open,
and a collapsed node shrinks to it. The redundant "Variable: <name>" row is
gone unless the variable was deleted ("<name>?" is the only hint then).
The Variable node sets its value when an exec flow arrives and exposes the
value at any time. Its output is read when a downstream node pulls it, not
frozen at the write, so count = count + 1 is one Variable node plus an Add.
Get variable keeps reading once per pulse: switching it would change the
behaviour of existing games.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.9 KiB
TypeScript
85 lines
3.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { CommandError } from '../../src/core/commands/Command';
|
||
import { CollapseNodesCommand } from '../../src/core/commands/graphCommands';
|
||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||
import { findAsset } from '../../src/core/model/Project';
|
||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||
import { collapsedNodeWidth, foldMarkKey } from '../../src/editor/graph/folding';
|
||
import { graphEditor, makeBehavior, makeProject, roundTrip } from '../helpers';
|
||
|
||
function threeNodes() {
|
||
const fixture = makeProject();
|
||
const behavior = makeBehavior(fixture, 'Mover');
|
||
const edit = graphEditor(fixture, behavior);
|
||
const start = edit.addNode('OnStart');
|
||
const self = edit.addNode('Self');
|
||
const move = edit.addNode('Move', { dx: 7 });
|
||
return { ...fixture, behavior, nodes: { start, self, move } };
|
||
}
|
||
|
||
describe('collapsing nodes', () => {
|
||
it('takes a whole selection back in one undo, each node to the state it really had', () => {
|
||
const { stack, behavior, nodes } = threeNodes();
|
||
stack.execute(new CollapseNodesCommand(behavior.id, [nodes.self.id], true));
|
||
stack.execute(new CollapseNodesCommand(behavior.id, [nodes.self.id, nodes.move.id], true));
|
||
expect(nodes.move.collapsed).toBe(true);
|
||
|
||
stack.undo();
|
||
expect(nodes.self.collapsed).toBe(true);
|
||
expect(nodes.move.collapsed).toBeUndefined();
|
||
});
|
||
|
||
it('changes no node when one of them is missing', () => {
|
||
const { project, behavior, nodes } = threeNodes();
|
||
const command = new CollapseNodesCommand(behavior.id, [nodes.self.id, 'no-such-node'], true);
|
||
expect(() => command.execute(project)).toThrow(CommandError);
|
||
expect(nodes.self.collapsed).toBeUndefined();
|
||
});
|
||
|
||
it('survives the save, and expanding leaves no trace in the file', () => {
|
||
const { project, stack, behavior, nodes } = threeNodes();
|
||
stack.execute(new CollapseNodesCommand(behavior.id, [nodes.move.id], true));
|
||
const loaded = findAsset(roundTrip(project), behavior.id) as BehaviorAsset;
|
||
expect(loaded.graph.nodes.find((n) => n.id === nodes.move.id)!.collapsed).toBe(true);
|
||
expect('collapsed' in loaded.graph.nodes.find((n) => n.id === nodes.self.id)!).toBe(false);
|
||
|
||
stack.execute(new CollapseNodesCommand(behavior.id, [nodes.move.id], false));
|
||
const again = findAsset(roundTrip(project), behavior.id) as BehaviorAsset;
|
||
expect(again.graph.nodes.every((n) => !('collapsed' in n))).toBe(true);
|
||
});
|
||
|
||
it('ignores a collapsed flag in the file that is not literally true', () => {
|
||
const { project, behavior } = threeNodes();
|
||
const raw = serializeProject(project) as {
|
||
assets: { id: string; graph?: { nodes: Record<string, unknown>[] } }[];
|
||
};
|
||
const stored = raw.assets.find((a) => a.id === behavior.id)!.graph!.nodes;
|
||
stored[0]!.collapsed = 'yes';
|
||
stored[1]!.collapsed = 1;
|
||
const loaded = findAsset(deserializeProject(raw), behavior.id) as BehaviorAsset;
|
||
expect(loaded.graph.nodes.every((n) => !('collapsed' in n))).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('the width of a collapsed node', () => {
|
||
it('fits the title plus its chrome, rounded up so the title is not cut', () => {
|
||
expect(collapsedNodeWidth(40.2, 31, 176)).toBe(72);
|
||
});
|
||
|
||
it('never grows past an open node – a long title is cut instead', () => {
|
||
expect(collapsedNodeWidth(300, 31, 176)).toBe(176);
|
||
});
|
||
});
|
||
|
||
describe('hidden links at a collapsed node', () => {
|
||
it('share one mark per side, because every port sits on the same point', () => {
|
||
const a = foldMarkKey({ node: 'n', port: 'x' }, false, true);
|
||
expect(foldMarkKey({ node: 'n', port: 'y' }, false, true)).toBe(a);
|
||
expect(foldMarkKey({ node: 'n', port: 'x' }, true, true)).not.toBe(a);
|
||
expect(foldMarkKey({ node: 'm', port: 'x' }, false, true)).not.toBe(a);
|
||
expect(foldMarkKey({ node: 'n', port: 'x' }, false)).not.toBe(
|
||
foldMarkKey({ node: 'n', port: 'y' }, false),
|
||
);
|
||
});
|
||
});
|