Files
feuerwerk/tests/core/collapsed-node.test.ts
rootandClaude Opus 5 e9fa592078 Add the Variable node and show the variable name in node titles
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>
2026-09-11 13:34:09 +02:00

85 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
);
});
});