import { describe, expect, it } from "vitest"; import type { Encounter } from "../types.js"; import { isDomainError } from "../types.js"; import { clearHistory, EMPTY_UNDO_REDO_STATE, pushUndo, redo, undo, } from "../undo-redo.js"; function enc(roundNumber = 1, activeIndex = 0): Encounter { return { combatants: [], activeIndex, roundNumber }; } describe("pushUndo", () => { it("adds a snapshot to the undo stack", () => { const result = pushUndo(EMPTY_UNDO_REDO_STATE, enc(1)); expect(result.undoStack).toHaveLength(1); expect(result.undoStack[0]).toEqual(enc(1)); }); it("clears the redo stack", () => { const state = { undoStack: [enc(1)], redoStack: [enc(2)], }; const result = pushUndo(state, enc(3)); expect(result.redoStack).toHaveLength(0); }); it("caps the undo stack at 50, dropping the oldest", () => { const undoStack = Array.from({ length: 50 }, (_, i) => enc(i + 1)); const state = { undoStack, redoStack: [] }; const result = pushUndo(state, enc(51)); expect(result.undoStack).toHaveLength(50); expect(result.undoStack[0]).toEqual(enc(2)); expect(result.undoStack[49]).toEqual(enc(51)); }); }); describe("undo", () => { it("pops from undo stack and pushes current to redo stack", () => { const state = { undoStack: [enc(1)], redoStack: [] }; const result = undo(state, enc(2)); expect(isDomainError(result)).toBe(false); if (isDomainError(result)) return; expect(result.encounter).toEqual(enc(1)); expect(result.state.undoStack).toHaveLength(0); expect(result.state.redoStack).toEqual([enc(2)]); }); it("returns domain error when undo stack is empty", () => { const result = undo(EMPTY_UNDO_REDO_STATE, enc(1)); expect(isDomainError(result)).toBe(true); if (!isDomainError(result)) return; expect(result.code).toBe("nothing-to-undo"); }); it("pops the most recent entry (last in stack)", () => { const state = { undoStack: [enc(1), enc(2), enc(3)], redoStack: [] }; const result = undo(state, enc(4)); expect(isDomainError(result)).toBe(false); if (isDomainError(result)) return; expect(result.encounter).toEqual(enc(3)); expect(result.state.undoStack).toEqual([enc(1), enc(2)]); }); }); describe("redo", () => { it("pops from redo stack and pushes current to undo stack", () => { const state = { undoStack: [], redoStack: [enc(1)] }; const result = redo(state, enc(2)); expect(isDomainError(result)).toBe(false); if (isDomainError(result)) return; expect(result.encounter).toEqual(enc(1)); expect(result.state.redoStack).toHaveLength(0); expect(result.state.undoStack).toEqual([enc(2)]); }); it("returns domain error when redo stack is empty", () => { const result = redo(EMPTY_UNDO_REDO_STATE, enc(1)); expect(isDomainError(result)).toBe(true); if (!isDomainError(result)) return; expect(result.code).toBe("nothing-to-redo"); }); it("pops the most recent entry (last in stack)", () => { const state = { undoStack: [], redoStack: [enc(1), enc(2), enc(3)] }; const result = redo(state, enc(4)); expect(isDomainError(result)).toBe(false); if (isDomainError(result)) return; expect(result.encounter).toEqual(enc(3)); expect(result.state.redoStack).toEqual([enc(1), enc(2)]); }); }); describe("undo-then-redo roundtrip", () => { it("returns the exact same encounter after undo then redo", () => { const original = enc(5); const current = enc(6); const afterPush = pushUndo(EMPTY_UNDO_REDO_STATE, original); const undoResult = undo(afterPush, current); expect(isDomainError(undoResult)).toBe(false); if (isDomainError(undoResult)) return; expect(undoResult.encounter).toEqual(original); const redoResult = redo(undoResult.state, undoResult.encounter); expect(isDomainError(redoResult)).toBe(false); if (isDomainError(redoResult)) return; expect(redoResult.encounter).toEqual(current); }); }); describe("clearHistory", () => { it("empties both stacks", () => { const result = clearHistory(); expect(result.undoStack).toHaveLength(0); expect(result.redoStack).toHaveLength(0); }); });