test: add Vitest setup with 115 tests (backend + frontend)
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* HistoryManager Tests – Undo/Redo
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { HistoryManager } from '../src/history/HistoryManager';
|
||||
import type { CADStateSnapshot } from '../src/history/HistoryManager';
|
||||
import type { CADElement, CADLayer, BlockDefinition } from '../src/types/cad.types';
|
||||
import type { ElementGroup } from '../src/tools/modification/GroupTool';
|
||||
import type { BackgroundConfig } from '../src/services/backgroundService';
|
||||
|
||||
function makeSnapshot(label: string, suffix: string = ''): Omit<CADStateSnapshot, 'timestamp' | 'label'> {
|
||||
const elements: CADElement[] = [
|
||||
{ id: `el-${suffix}-1`, type: 'rect', layerId: 'layer-1', x: 0, y: 0, width: 10, height: 10, properties: {} },
|
||||
{ id: `el-${suffix}-2`, type: 'circle', layerId: 'layer-1', x: 50, y: 50, width: 20, height: 20, properties: {} },
|
||||
];
|
||||
const layers: CADLayer[] = [
|
||||
{ id: 'layer-1', name: 'Layer 1', visible: true, locked: false, color: '#ffffff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null },
|
||||
];
|
||||
const blocks: BlockDefinition[] = [];
|
||||
const groups: ElementGroup[] = [];
|
||||
const bgConfig: BackgroundConfig | null = null;
|
||||
return { elements, layers, blocks, groups, bgConfig };
|
||||
}
|
||||
|
||||
describe('HistoryManager', () => {
|
||||
let hm: HistoryManager;
|
||||
|
||||
beforeEach(() => {
|
||||
hm = new HistoryManager();
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should set initial state without undo history', () => {
|
||||
const snap = makeSnapshot('Initial');
|
||||
hm.initialize(snap);
|
||||
expect(hm.getCurrentState()).not.toBeNull();
|
||||
expect(hm.getCurrentState()?.label).toBe('Initial');
|
||||
expect(hm.canUndo()).toBe(false);
|
||||
expect(hm.canRedo()).toBe(false);
|
||||
});
|
||||
|
||||
it('should reset undo and redo stacks on initialize', () => {
|
||||
const snap = makeSnapshot('Initial');
|
||||
hm.initialize(snap);
|
||||
hm.pushSnapshot(makeSnapshot('Change 1'), 'Change 1');
|
||||
hm.pushSnapshot(makeSnapshot('Change 2'), 'Change 2');
|
||||
|
||||
// Re-initialize should clear history
|
||||
hm.initialize(snap);
|
||||
expect(hm.canUndo()).toBe(false);
|
||||
expect(hm.canRedo()).toBe(false);
|
||||
expect(hm.getUndoCount()).toBe(0);
|
||||
expect(hm.getRedoCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pushSnapshot', () => {
|
||||
it('should push current state to undo stack and set new state', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
|
||||
expect(hm.getCurrentState()?.label).toBe('Change 1');
|
||||
expect(hm.canUndo()).toBe(true);
|
||||
expect(hm.getUndoCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('should clear redo stack on new push', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.undo(); // Now redo stack has 1 entry
|
||||
expect(hm.canRedo()).toBe(true);
|
||||
|
||||
hm.pushSnapshot(makeSnapshot('Change 2', 'v2'), 'Change 2');
|
||||
expect(hm.canRedo()).toBe(false);
|
||||
expect(hm.getRedoCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should store multiple snapshots', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
hm.pushSnapshot(makeSnapshot(`Change ${i}`, `v${i}`), `Change ${i}`);
|
||||
}
|
||||
expect(hm.getUndoCount()).toBe(5);
|
||||
expect(hm.getCurrentState()?.label).toBe('Change 5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('undo', () => {
|
||||
it('should restore previous state', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
|
||||
const prev = hm.undo();
|
||||
expect(prev).not.toBeNull();
|
||||
expect(prev?.label).toBe('Initial');
|
||||
expect(hm.getCurrentState()?.label).toBe('Initial');
|
||||
});
|
||||
|
||||
it('should return null when no undo available', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
const result = hm.undo();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should move current state to redo stack', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.undo();
|
||||
|
||||
expect(hm.canRedo()).toBe(true);
|
||||
expect(hm.getRedoCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle multiple undos', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.pushSnapshot(makeSnapshot('Change 2', 'v2'), 'Change 2');
|
||||
hm.pushSnapshot(makeSnapshot('Change 3', 'v3'), 'Change 3');
|
||||
|
||||
hm.undo();
|
||||
expect(hm.getCurrentState()?.label).toBe('Change 2');
|
||||
hm.undo();
|
||||
expect(hm.getCurrentState()?.label).toBe('Change 1');
|
||||
hm.undo();
|
||||
expect(hm.getCurrentState()?.label).toBe('Initial');
|
||||
expect(hm.canUndo()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redo', () => {
|
||||
it('should restore next state after undo', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.undo();
|
||||
|
||||
const next = hm.redo();
|
||||
expect(next).not.toBeNull();
|
||||
expect(next?.label).toBe('Change 1');
|
||||
expect(hm.getCurrentState()?.label).toBe('Change 1');
|
||||
});
|
||||
|
||||
it('should return null when no redo available', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
const result = hm.redo();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should move current state back to undo stack', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.undo();
|
||||
hm.redo();
|
||||
|
||||
expect(hm.canUndo()).toBe(true);
|
||||
expect(hm.getUndoCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle multiple redos', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.pushSnapshot(makeSnapshot('Change 2', 'v2'), 'Change 2');
|
||||
hm.pushSnapshot(makeSnapshot('Change 3', 'v3'), 'Change 3');
|
||||
|
||||
// Undo all 3
|
||||
hm.undo(); hm.undo(); hm.undo();
|
||||
expect(hm.getCurrentState()?.label).toBe('Initial');
|
||||
|
||||
// Redo all 3
|
||||
hm.redo();
|
||||
expect(hm.getCurrentState()?.label).toBe('Change 1');
|
||||
hm.redo();
|
||||
expect(hm.getCurrentState()?.label).toBe('Change 2');
|
||||
hm.redo();
|
||||
expect(hm.getCurrentState()?.label).toBe('Change 3');
|
||||
expect(hm.canRedo()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canUndo & canRedo', () => {
|
||||
it('canUndo should be false initially', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
expect(hm.canUndo()).toBe(false);
|
||||
});
|
||||
|
||||
it('canUndo should be true after push', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
expect(hm.canUndo()).toBe(true);
|
||||
});
|
||||
|
||||
it('canRedo should be false initially', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
expect(hm.canRedo()).toBe(false);
|
||||
});
|
||||
|
||||
it('canRedo should be true after undo', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.undo();
|
||||
expect(hm.canRedo()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistory', () => {
|
||||
it('should return history entries with current marked', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.pushSnapshot(makeSnapshot('Change 2', 'v2'), 'Change 2');
|
||||
|
||||
const history = hm.getHistory();
|
||||
expect(history.length).toBe(3); // 2 undo + 1 current
|
||||
const currentEntries = history.filter(e => e.isCurrent);
|
||||
expect(currentEntries.length).toBe(1);
|
||||
expect(currentEntries[0].label).toBe('Change 2');
|
||||
});
|
||||
|
||||
it('should include redo entries after undo', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.undo();
|
||||
|
||||
const history = hm.getHistory();
|
||||
// 0 undo + 1 current (Initial) + 1 redo (Change 1)
|
||||
expect(history.length).toBe(2);
|
||||
const currentEntries = history.filter(e => e.isCurrent);
|
||||
expect(currentEntries.length).toBe(1);
|
||||
expect(currentEntries[0].label).toBe('Initial');
|
||||
});
|
||||
|
||||
it('should return empty-ish history for fresh manager', () => {
|
||||
const history = hm.getHistory();
|
||||
expect(history.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('jumpTo', () => {
|
||||
it('should jump to a specific undo entry', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.pushSnapshot(makeSnapshot('Change 2', 'v2'), 'Change 2');
|
||||
hm.pushSnapshot(makeSnapshot('Change 3', 'v3'), 'Change 3');
|
||||
|
||||
// Jump to undo-0 (Initial state)
|
||||
const result = hm.jumpTo('undo-0');
|
||||
expect(result).not.toBeNull();
|
||||
expect(hm.getCurrentState()?.label).toBe('Initial');
|
||||
});
|
||||
|
||||
it('should jump to current', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
|
||||
const result = hm.jumpTo('current');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.label).toBe('Change 1');
|
||||
});
|
||||
|
||||
it('should jump to a redo entry', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.pushSnapshot(makeSnapshot('Change 2', 'v2'), 'Change 2');
|
||||
hm.undo();
|
||||
hm.undo();
|
||||
|
||||
// Jump to redo-1 (Change 2)
|
||||
const result = hm.jumpTo('redo-1');
|
||||
expect(result).not.toBeNull();
|
||||
expect(hm.getCurrentState()?.label).toBe('Change 2');
|
||||
});
|
||||
|
||||
it('should return null for unknown entry id', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
const result = hm.jumpTo('unknown-id');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear all history', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
hm.pushSnapshot(makeSnapshot('Change 2', 'v2'), 'Change 2');
|
||||
|
||||
hm.clear();
|
||||
expect(hm.getCurrentState()).toBeNull();
|
||||
expect(hm.canUndo()).toBe(false);
|
||||
expect(hm.canRedo()).toBe(false);
|
||||
expect(hm.getUndoCount()).toBe(0);
|
||||
expect(hm.getRedoCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
it('should notify listeners on state change', () => {
|
||||
let callCount = 0;
|
||||
const unsubscribe = hm.subscribe(() => callCount++);
|
||||
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
expect(callCount).toBe(1);
|
||||
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
expect(callCount).toBe(2);
|
||||
|
||||
hm.undo();
|
||||
expect(callCount).toBe(3);
|
||||
|
||||
unsubscribe();
|
||||
hm.pushSnapshot(makeSnapshot('Change 2', 'v2'), 'Change 2');
|
||||
expect(callCount).toBe(3); // No new calls after unsubscribe
|
||||
});
|
||||
|
||||
it('should notify on clear', () => {
|
||||
let callCount = 0;
|
||||
hm.subscribe(() => callCount++);
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
hm.clear();
|
||||
expect(callCount).toBe(2); // init + clear
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxStackSize', () => {
|
||||
it('should limit undo stack size', () => {
|
||||
const smallHM = new HistoryManager({ maxStackSize: 3 });
|
||||
smallHM.initialize(makeSnapshot('Initial'));
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
smallHM.pushSnapshot(makeSnapshot(`Change ${i}`, `v${i}`), `Change ${i}`);
|
||||
}
|
||||
|
||||
expect(smallHM.getUndoCount()).toBe(3);
|
||||
});
|
||||
|
||||
it('should use default max size of 100', () => {
|
||||
const defaultHM = new HistoryManager();
|
||||
defaultHM.initialize(makeSnapshot('Initial'));
|
||||
|
||||
for (let i = 1; i <= 150; i++) {
|
||||
defaultHM.pushSnapshot(makeSnapshot(`Change ${i}`, `v${i}`), `Change ${i}`);
|
||||
}
|
||||
|
||||
expect(defaultHM.getUndoCount()).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUndoCount & getRedoCount', () => {
|
||||
it('should track counts correctly', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
expect(hm.getUndoCount()).toBe(0);
|
||||
expect(hm.getRedoCount()).toBe(0);
|
||||
|
||||
hm.pushSnapshot(makeSnapshot('Change 1', 'v1'), 'Change 1');
|
||||
expect(hm.getUndoCount()).toBe(1);
|
||||
expect(hm.getRedoCount()).toBe(0);
|
||||
|
||||
hm.undo();
|
||||
expect(hm.getUndoCount()).toBe(0);
|
||||
expect(hm.getRedoCount()).toBe(1);
|
||||
|
||||
hm.redo();
|
||||
expect(hm.getUndoCount()).toBe(1);
|
||||
expect(hm.getRedoCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapshot data integrity', () => {
|
||||
it('should preserve elements in snapshots', () => {
|
||||
const snap = makeSnapshot('Test');
|
||||
hm.initialize(snap);
|
||||
const current = hm.getCurrentState();
|
||||
expect(current?.elements.length).toBe(2);
|
||||
expect(current?.elements[0].id).toBe('el--1');
|
||||
});
|
||||
|
||||
it('should preserve different element sets across snapshots', () => {
|
||||
hm.initialize(makeSnapshot('Initial'));
|
||||
const snap1 = makeSnapshot('Change 1', 'v1');
|
||||
hm.pushSnapshot(snap1, 'Change 1');
|
||||
|
||||
hm.undo();
|
||||
const afterUndo = hm.getCurrentState();
|
||||
expect(afterUndo?.elements[0].id).toBe('el--1'); // Initial state
|
||||
|
||||
hm.redo();
|
||||
const afterRedo = hm.getCurrentState();
|
||||
expect(afterRedo?.elements[0].id).toBe('el-v1-1'); // Change 1 state
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user