import { describe, expect, it } from 'vitest'; import { CommandError } from '../../src/core/commands/Command'; import { ConnectCommand, FoldEdgeCommand } from '../../src/core/commands/graphCommands'; import { GraphInterpreter } from '../../src/core/graph'; import type { BehaviorAsset } from '../../src/core/model/Asset'; import { findAsset } from '../../src/core/model/Project'; import { deserializeProject, serializeProject } from '../../src/core/store/serialize'; import { foldedEdgeShown, foldMarkKey, foldMarkLabel } from '../../src/editor/graph/folding'; import { fakeContext, graphEditor, makeBehavior, makeProject, roundTrip } from '../helpers'; /** "On start → Move(self, dx = 7)": two edges, so a fold can hide one and leave the other. */ function mover() { 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 }); edit.wire([ [start, 'exec', move, 'exec'], [self, 'object', move, 'object'], ]); return { ...fixture, behavior, edit, nodes: { start, self, move } }; } describe('folding an edge', () => { it('changes nothing about what the graph does – the value still arrives', () => { const { project, stack, behavior } = mover(); for (const edge of behavior.graph.edges) stack.execute(new FoldEdgeCommand(behavior.id, edge.id, true)); expect(behavior.graph.edges.every((e) => e.folded === true)).toBe(true); const ctx = fakeContext(); new GraphInterpreter(behavior.graph).fire('OnStart', ctx); expect(ctx.self.x).toBe(7); // And the edges themselves are untouched apart from the flag. expect(findAsset(project, behavior.id)).toBe(behavior); expect(behavior.graph.edges).toHaveLength(2); }); it('survives the save, and unfolding leaves no trace in the file', () => { const { project, stack, behavior } = mover(); const [first, second] = behavior.graph.edges; stack.execute(new FoldEdgeCommand(behavior.id, first!.id, true)); const loaded = findAsset(roundTrip(project), behavior.id) as BehaviorAsset; expect(loaded.graph.edges.find((e) => e.id === first!.id)!.folded).toBe(true); expect('folded' in loaded.graph.edges.find((e) => e.id === second!.id)!).toBe(false); stack.execute(new FoldEdgeCommand(behavior.id, first!.id, false)); const again = findAsset(roundTrip(project), behavior.id) as BehaviorAsset; expect(again.graph.edges.every((e) => !('folded' in e))).toBe(true); }); it('is undoable, and undo restores the state the edge really had', () => { const { stack, behavior } = mover(); const edge = behavior.graph.edges[0]!; stack.execute(new FoldEdgeCommand(behavior.id, edge.id, true)); // Folding an already folded edge must not make undo unfold it. stack.execute(new FoldEdgeCommand(behavior.id, edge.id, true)); stack.undo(); expect(edge.folded).toBe(true); stack.undo(); expect(edge.folded).toBeUndefined(); }); it('reports a missing edge as a CommandError instead of throwing from the constructor', () => { const { project, behavior } = mover(); const command = new FoldEdgeCommand(behavior.id, 'no-such-edge', true); expect(command.label).toBe('Hide link'); expect(() => command.execute(project)).toThrow(CommandError); }); it('belongs to the edge, not to the port: a replacement starts out shown', () => { const { stack, behavior, edit, nodes } = mover(); const edge = behavior.graph.edges.find((e) => e.to.port === 'object')!; stack.execute(new FoldEdgeCommand(behavior.id, edge.id, true)); const owner = edit.addNode('Owner'); stack.execute( new ConnectCommand( behavior.id, { node: owner.id, port: 'object' }, { node: nodes.move.id, port: 'object' }, ), ); const now = behavior.graph.edges.find((e) => e.to.port === 'object')!; expect(now.id).not.toBe(edge.id); expect(now.folded).toBeUndefined(); // Undo puts the old edge back exactly as it was – folded. stack.undo(); expect(behavior.graph.edges.find((e) => e.to.port === 'object')!.folded).toBe(true); }); it('ignores a folded flag in the file that is not literally true', () => { const { project, behavior } = mover(); const raw = serializeProject(project) as { assets: { id: string; graph?: { edges: Record[] } }[]; }; const edges = raw.assets.find((a) => a.id === behavior.id)!.graph!.edges; edges[0]!.folded = 'yes'; edges[1]!.folded = false; const loaded = findAsset(deserializeProject(raw), behavior.id) as BehaviorAsset; expect(loaded.graph.edges.every((e) => !('folded' in e))).toBe(true); }); }); describe('when a folded edge shows its curve anyway', () => { const edge = { id: 'e', from: { node: 'a', port: 'out' }, to: { node: 'b', port: 'in' } }; it('stays hidden while neither end is selected', () => { expect(foldedEdgeShown(edge, new Set(['c']), false)).toBe(false); expect(foldedEdgeShown(edge, new Set(), false)).toBe(false); }); it('comes back for either end, not just the source', () => { expect(foldedEdgeShown(edge, new Set(['a']), false)).toBe(true); expect(foldedEdgeShown(edge, new Set(['b']), false)).toBe(true); }); it('always shows while a pulse is on it, so the live trace keeps its thread', () => { expect(foldedEdgeShown(edge, new Set(), true)).toBe(true); }); }); describe('several hidden links at one port', () => { it('share a mark, while a different port or side gets its own', () => { const out = foldMarkKey({ node: 'a', port: 'value' }, true); expect(foldMarkKey({ node: 'a', port: 'value' }, true)).toBe(out); expect(foldMarkKey({ node: 'a', port: 'other' }, true)).not.toBe(out); expect(foldMarkKey({ node: 'b', port: 'value' }, true)).not.toBe(out); // The same port on the other side of the node is a mark of its own. expect(foldMarkKey({ node: 'a', port: 'value' }, false)).not.toBe(out); }); it('cannot be confused by a node or port name that contains the separator', () => { expect(foldMarkKey({ node: 'a b', port: 'c' }, true)).not.toBe( foldMarkKey({ node: 'a', port: 'b c' }, true), ); }); it('name one far node and count the rest, so nothing is written on top of anything', () => { expect(foldMarkLabel(['Add'])).toBe('Add'); expect(foldMarkLabel(['Add', 'Compare'])).toBe('Add +1'); expect(foldMarkLabel(['Add', 'Compare', 'Multiply'])).toBe('Add +2'); expect(foldMarkLabel([])).toBe(''); }); });