A chevron in front of the title toggles it without touching the selection, and right-click offers Collapse node / Expand node for the whole selection as one undo step. A collapsed node draws its title bar alone; every link of one side meets at a single point on it, and hidden links there share one mark per side, since per-port marks would stack on the same pixels. It has no ports to grab, so a link dropped on it connects nothing. Like hiding a link this is drawing only: collapsed is saved on the node and an expanded node leaves no trace in the project file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75 lines
3.5 KiB
TypeScript
75 lines
3.5 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 { 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('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),
|
|
);
|
|
});
|
|
});
|