/** * Undo/Redo für das Projekt-Modell (`src/state/historySlice.ts` + * `setProject`-Wrapper in `src/state/projectSlice.ts`). * * • Ein diskreter Store-Aufruf (z. B. `toggleLevelVisible`) muss GENAU einen * Undo-Schritt erzeugen. * • `undo`/`redo` müssen den jeweils anderen Stack korrekt befüllen und * dürfen bei leerem Stack nicht crashen (No-Op). * • Eine neue Aktion nach `undo()` muss den Redo-Stack verwerfen. * • Hochfrequente Drag-Aktionen (z. B. `moveGripOf`, bei JEDEM Maus-Move- * Schritt aufgerufen statt über ein Draft/Commit-Muster) müssen zu EINEM * Undo-Schritt koalesziert werden, nicht zu einem pro Aufruf. */ import { describe, it, expect, beforeEach } from "vitest"; import { getState, setState } from "./appStore"; import { sampleProject } from "../model/sampleProject"; import { HISTORY_LIMIT } from "./historySlice"; // Frischen Ausgangszustand vor jedem Test herstellen: eigenes Projekt + // leere Stacks, damit die Tests unabhängig voneinander laufen (derselbe // Store ist ein Singleton über die ganze Datei hinweg). beforeEach(() => { setState({ project: sampleProject, undoStack: [], redoStack: [], canUndo: false, canRedo: false, }); }); describe("Undo/Redo", () => { it("undo macht eine Projekt-Änderung rückgängig", () => { const before = getState().project; getState().toggleLevelVisible("eg"); const afterToggle = getState().project; expect(afterToggle).not.toBe(before); expect(afterToggle.drawingLevels.find((l) => l.id === "eg")?.visible).toBe(false); expect(getState().canUndo).toBe(true); getState().undo(); expect(getState().project.drawingLevels.find((l) => l.id === "eg")?.visible).toBe(true); expect(getState().canUndo).toBe(false); expect(getState().canRedo).toBe(true); }); it("redo stellt die rückgängig gemachte Änderung wieder her", () => { getState().toggleLevelVisible("eg"); getState().undo(); getState().redo(); expect(getState().project.drawingLevels.find((l) => l.id === "eg")?.visible).toBe(false); expect(getState().canRedo).toBe(false); expect(getState().canUndo).toBe(true); }); it("undo bei leerem Stack ist ein No-Op (kein Crash)", () => { expect(getState().undoStack.length).toBe(0); const before = getState().project; expect(() => getState().undo()).not.toThrow(); expect(getState().project).toBe(before); expect(getState().canUndo).toBe(false); }); it("redo bei leerem Stack ist ein No-Op (kein Crash)", () => { expect(getState().redoStack.length).toBe(0); const before = getState().project; expect(() => getState().redo()).not.toThrow(); expect(getState().project).toBe(before); expect(getState().canRedo).toBe(false); }); it("eine neue Aktion nach undo() verwirft den Redo-Stack", () => { getState().toggleLevelVisible("eg"); getState().undo(); expect(getState().redoStack.length).toBe(1); getState().toggleLevelVisible("og"); expect(getState().redoStack.length).toBe(0); expect(getState().canRedo).toBe(false); // Die neue Aktion selbst bleibt natürlich undo-fähig. expect(getState().canUndo).toBe(true); }); it("die allererste Änderung bleibt exakt EIN Undo-Schritt (kein Start-Eintrag)", () => { expect(getState().undoStack.length).toBe(0); getState().toggleLevelVisible("eg"); expect(getState().undoStack.length).toBe(1); }); it("history-Stack verwirft die ältesten Einträge über HISTORY_LIMIT hinaus", () => { for (let i = 0; i < HISTORY_LIMIT + 10; i++) { getState().toggleLevelVisible("eg"); } expect(getState().undoStack.length).toBe(HISTORY_LIMIT); }); it("ein Grip-Drag (viele setProject-Aufrufe in Serie) koalesziert zu EINEM Undo-Schritt", () => { const w1Before = getState().project.walls.find((w) => w.id === "W1"); expect(w1Before?.start).toEqual({ x: 0, y: 0 }); // Simuliert einen Drag: viele schnelle moveGripOf-Aufrufe auf DASSELBE // Element/denselben Griff, wie sie bei jedem Maus-Move-Event entstehen // (siehe gripHandlers.onGripMove in App.tsx → moveGripOf). for (let i = 1; i <= 20; i++) { getState().moveGripOf(null, "W1", 0, { x: i * 0.1, y: 0 }); } const w1After = getState().project.walls.find((w) => w.id === "W1"); expect(w1After?.start).toEqual({ x: 2, y: 0 }); // Trotz 20 Aufrufen: genau EIN Undo-Schritt. expect(getState().undoStack.length).toBe(1); getState().undo(); const w1Restored = getState().project.walls.find((w) => w.id === "W1"); expect(w1Restored?.start).toEqual({ x: 0, y: 0 }); expect(getState().undoStack.length).toBe(0); }); it("zwei Drags auf UNTERSCHIEDLICHE Griffe erzeugen ZWEI Undo-Schritte", () => { getState().moveGripOf(null, "W1", 0, { x: 1, y: 0 }); getState().moveGripOf(null, "W1", 1, { x: 6, y: 0 }); expect(getState().undoStack.length).toBe(2); }); });