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[] } }[]; }; 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), ); }); });