// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) import {act} from '@testing-library/react'; import useFlowStore from '../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx'; import { mockReactFlow } from '../../../setupFlowTests.ts'; beforeAll(() => { mockReactFlow(); }); describe("UndoRedo Middleware", () => { beforeEach(() => { jest.useFakeTimers(); }); test("pushSnapshot adds a snapshot to past and clears future", () => { const store = useFlowStore; store.setState({ nodes: [{ id: 'A', type: 'default', position: {x: 0, y: 0}, data: {label: 'A'} }], edges: [], past: [], future: [{ nodes: [ { id: 'A', type: 'default', position: {x: 0, y: 0}, data: {label: 'A'} } ], edges: [], warnings: { warningRegistry: new Map(), severityIndex: new Map() } }], ruleRegistry: new Map(), editorWarningRegistry: new Map(), severityIndex: new Map() }); act(() => { store.getState().pushSnapshot(); }) const state = store.getState(); expect(state.past.length).toBe(1); expect(state.past[0]).toEqual({ nodes: [{ id: 'A', type: 'default', position: {x: 0, y: 0}, data: {label: 'A'} }], edges: [], warnings: { warningRegistry: {}, severityIndex: {} } }); expect(state.future).toEqual([]); }); test("pushSnapshot does nothing during batch action", () => { const store = useFlowStore; act(() => { store.setState({ isBatchAction: true }); store.getState().pushSnapshot(); }) expect(store.getState().past.length).toBe(0); }); test("undo restores last snapshot and pushes current snapshot to future", () => { const store = useFlowStore; // initial state store.setState({ nodes: [{ id: 'A', type: 'default', position: {x: 0, y: 0}, data: {label: 'A'} }], edges: [], editorWarningRegistry: new Map(), severityIndex: new Map() }); act(() => { store.getState().pushSnapshot(); // modified state store.setState({ nodes: [{ id: 'B', type: 'default', position: {x: 0, y: 0}, data: {label: 'B'} }], edges: [] }); store.getState().undo(); }) expect(store.getState().nodes).toEqual([{ id: 'A', type: 'default', position: {x: 0, y: 0}, data: {label: 'A'} }]); expect(store.getState().future.length).toBe(1); expect(store.getState().future[0]).toEqual({ nodes: [{ id: 'B', type: 'default', position: {x: 0, y: 0}, data: {label: 'B'} }], edges: [], warnings: { warningRegistry: {}, severityIndex: {} } }); }); test("undo does nothing when past is empty", () => { const store = useFlowStore; store.setState({past: []}); act(() => { store.getState().undo(); }); expect(store.getState().nodes).toEqual([]); expect(store.getState().future).toEqual([]); }); test("redo restores last future snapshot and pushes current to past", () => { const store = useFlowStore; // initial store.setState({ nodes: [{ id: 'A', type: 'default', position: {x: 0, y: 0}, data: {label: 'A'} }], edges: [], editorWarningRegistry: new Map(), severityIndex: new Map() }); act(() => { store.getState().pushSnapshot(); store.setState({ nodes: [{ id: 'B', type: 'default', position: {x: 0, y: 0}, data: {label: 'B'} }], edges: [] }); store.getState().undo(); // redo should restore node with id 'B' store.getState().redo(); }) expect(store.getState().nodes).toEqual([{ id: 'B', type: 'default', position: {x: 0, y: 0}, data: {label: 'B'} }]); expect(store.getState().past.length).toBe(1); // snapshot A stored expect(store.getState().past[0]).toEqual({ nodes: [{ id: 'A', type: 'default', position: {x: 0, y: 0}, data: {label: 'A'} }], edges: [], warnings: { warningRegistry: {}, severityIndex: {} } }); }); test("redo does nothing when future is empty", () => { const store = useFlowStore; store.setState({past: []}); act(() => { store.getState().redo(); }); expect(store.getState().nodes).toEqual([]); }); test("beginBatchAction pushes snapshot and sets isBatchAction=true", () => { const store = useFlowStore; store.setState({ nodes: [{ id: 'A', type: 'default', position: {x: 0, y: 0}, data: {label: 'A'} }], edges: [], editorWarningRegistry: new Map(), severityIndex: new Map() }); act(() => { store.getState().beginBatchAction(); }); expect(store.getState().isBatchAction).toBe(true); expect(store.getState().past.length).toBe(1); }); test("endBatchAction sets isBatchAction=false after timeout", () => { const store = useFlowStore; store.setState({ isBatchAction: true }); act(() => { store.getState().endBatchAction(); }); // isBatchAction should remain true before the timer has advanced expect(store.getState().isBatchAction).toBe(true); jest.advanceTimersByTime(10); // it should now be set to false as the timer has advanced enough expect(store.getState().isBatchAction).toBe(false); }); test("multiple beginBatchAction calls clear the timeout", () => { const store = useFlowStore; act(() => { store.getState().beginBatchAction(); store.getState().endBatchAction(); // starts timeout store.getState().beginBatchAction(); // should clear previous timeout }); jest.advanceTimersByTime(10); // After advancing the timers, isBatchAction should still be true, // as the timeout should have been cleared expect(store.getState().isBatchAction).toBe(true); }); });