glam/frontend/tests/unit/history-context.test.tsx
kempersc 2761857b0d Add scripts for converting OWL/Turtle ontology to Mermaid and PlantUML diagrams
- Implemented `owl_to_mermaid.py` to convert OWL/Turtle files into Mermaid class diagrams.
- Implemented `owl_to_plantuml.py` to convert OWL/Turtle files into PlantUML class diagrams.
- Added two new PlantUML files for custodian multi-aspect diagrams.
2025-11-22 23:01:13 +01:00

333 lines
9.8 KiB
TypeScript

/**
* Tests for HistoryContext
*/
import { describe, it, expect, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { HistoryProvider, useHistory, useHistoryKeyboard } from '@/contexts/HistoryContext';
import React from 'react';
// Wrapper component for testing
const createWrapper = (initialState?: any, onStateChange?: (state: any) => void) => {
return ({ children }: { children: React.ReactNode }) => (
<HistoryProvider initialState={initialState} onStateChange={onStateChange}>
{children}
</HistoryProvider>
);
};
describe('HistoryContext', () => {
it('should initialize with no history', () => {
const wrapper = createWrapper();
const { result } = renderHook(() => useHistory(), { wrapper });
expect(result.current.canUndo).toBe(false);
expect(result.current.canRedo).toBe(false);
expect(result.current.historySize).toBe(0);
expect(result.current.currentEntry).toBeNull();
});
it('should initialize with initial state', () => {
const initialState = { count: 0 };
const wrapper = createWrapper(initialState);
const { result } = renderHook(() => useHistory(), { wrapper });
expect(result.current.currentEntry).not.toBeNull();
expect(result.current.currentEntry?.state).toEqual(initialState);
expect(result.current.currentEntry?.action).toBe('Initial state');
expect(result.current.historySize).toBe(1);
});
it('should push new state to history', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
act(() => {
result.current.pushHistory({ count: 1 }, 'Increment');
});
expect(result.current.currentEntry?.state).toEqual({ count: 1 });
expect(result.current.currentEntry?.action).toBe('Increment');
expect(result.current.canUndo).toBe(true);
expect(result.current.canRedo).toBe(false);
expect(result.current.historySize).toBe(2);
});
it('should undo to previous state', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
act(() => {
result.current.pushHistory({ count: 1 }, 'Increment');
result.current.pushHistory({ count: 2 }, 'Increment');
});
expect(result.current.currentEntry?.state).toEqual({ count: 2 });
let previousState: any;
act(() => {
previousState = result.current.undo();
});
expect(previousState).toEqual({ count: 1 });
expect(result.current.currentEntry?.state).toEqual({ count: 1 });
expect(result.current.canUndo).toBe(true);
expect(result.current.canRedo).toBe(true);
});
it('should redo to next state', async () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
act(() => {
result.current.pushHistory({ count: 1 }, 'Increment');
});
act(() => {
result.current.pushHistory({ count: 2 }, 'Increment');
});
act(() => {
result.current.undo();
});
expect(result.current.currentEntry?.state).toEqual({ count: 1 });
let nextState: any;
act(() => {
nextState = result.current.redo();
});
expect(nextState).toEqual({ count: 2 });
expect(result.current.currentEntry?.state).toEqual({ count: 2 });
expect(result.current.canUndo).toBe(true);
expect(result.current.canRedo).toBe(false);
});
it('should return null when undoing with no past', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
let previousState: any;
act(() => {
previousState = result.current.undo();
});
expect(previousState).toBeNull();
expect(result.current.canUndo).toBe(false);
});
it('should return null when redoing with no future', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
let nextState: any;
act(() => {
nextState = result.current.redo();
});
expect(nextState).toBeNull();
expect(result.current.canRedo).toBe(false);
});
it('should clear future when new action is pushed', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
act(() => {
result.current.pushHistory({ count: 1 }, 'Increment');
});
act(() => {
result.current.pushHistory({ count: 2 }, 'Increment');
});
act(() => {
result.current.undo();
});
expect(result.current.canRedo).toBe(true);
act(() => {
result.current.pushHistory({ count: 10 }, 'Set to 10');
});
expect(result.current.canRedo).toBe(false);
expect(result.current.currentEntry?.state).toEqual({ count: 10 });
});
it('should clear all history', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
act(() => {
result.current.pushHistory({ count: 1 }, 'Increment');
result.current.pushHistory({ count: 2 }, 'Increment');
});
expect(result.current.historySize).toBe(3);
act(() => {
result.current.clearHistory();
});
expect(result.current.historySize).toBe(0);
expect(result.current.canUndo).toBe(false);
expect(result.current.canRedo).toBe(false);
expect(result.current.currentEntry).toBeNull();
});
it('should jump to specific entry', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
act(() => {
result.current.pushHistory({ count: 1 }, 'Increment');
result.current.pushHistory({ count: 2 }, 'Increment');
result.current.pushHistory({ count: 3 }, 'Increment');
});
let targetState: any;
act(() => {
targetState = result.current.jumpToEntry(1); // Jump to count: 1
});
expect(targetState).toEqual({ count: 1 });
expect(result.current.currentEntry?.state).toEqual({ count: 1 });
expect(result.current.canUndo).toBe(true);
expect(result.current.canRedo).toBe(true);
});
it('should return null when jumping to invalid index', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
let targetState: any;
act(() => {
targetState = result.current.jumpToEntry(-1);
});
expect(targetState).toBeNull();
act(() => {
targetState = result.current.jumpToEntry(100);
});
expect(targetState).toBeNull();
});
it('should respect max history size', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<HistoryProvider initialState={{ count: 0 }} maxHistorySize={3}>
{children}
</HistoryProvider>
);
const { result } = renderHook(() => useHistory(), { wrapper });
act(() => {
for (let i = 1; i <= 10; i++) {
result.current.pushHistory({ count: i }, `Set to ${i}`);
}
});
// Max 3 past + 1 present = 4 total
expect(result.current.historySize).toBeLessThanOrEqual(4);
});
it('should get past and future entries', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(() => useHistory(), { wrapper });
act(() => {
result.current.pushHistory({ count: 1 }, 'Increment');
});
act(() => {
result.current.pushHistory({ count: 2 }, 'Increment');
});
act(() => {
result.current.undo();
});
const pastEntries = result.current.getPastEntries();
const futureEntries = result.current.getFutureEntries();
// After undo: past=[count:0], present=[count:1], future=[count:2]
expect(pastEntries.length).toBe(1); // Only initial state
expect(futureEntries.length).toBe(1); // The undone state
expect(pastEntries[0].state).toEqual({ count: 0 });
expect(result.current.currentEntry?.state).toEqual({ count: 1 });
expect(futureEntries[0].state).toEqual({ count: 2 });
});
it('should call onStateChange when state changes', () => {
const onStateChange = vi.fn();
const wrapper = createWrapper({ count: 0 }, onStateChange);
const { result } = renderHook(() => useHistory(), { wrapper });
act(() => {
result.current.pushHistory({ count: 1 }, 'Increment');
});
expect(onStateChange).toHaveBeenCalledWith({ count: 1 });
});
it('should throw error when useHistory is called outside provider', () => {
// Suppress console.error for this test
const consoleError = console.error;
console.error = vi.fn();
expect(() => {
renderHook(() => useHistory());
}).toThrow('useHistory must be used within a HistoryProvider');
console.error = consoleError;
});
});
describe('useHistoryKeyboard', () => {
it('should handle keyboard shortcuts for undo/redo', () => {
const wrapper = createWrapper({ count: 0 });
const { result } = renderHook(
() => {
const history = useHistory();
useHistoryKeyboard();
return history;
},
{ wrapper }
);
act(() => {
result.current.pushHistory({ count: 1 }, 'Increment');
result.current.pushHistory({ count: 2 }, 'Increment');
});
expect(result.current.currentEntry?.state).toEqual({ count: 2 });
// Simulate Ctrl+Z (undo)
act(() => {
const event = new KeyboardEvent('keydown', {
key: 'z',
ctrlKey: true,
bubbles: true,
});
window.dispatchEvent(event);
});
expect(result.current.currentEntry?.state).toEqual({ count: 1 });
// Simulate Ctrl+Shift+Z (redo)
act(() => {
const event = new KeyboardEvent('keydown', {
key: 'z',
ctrlKey: true,
shiftKey: true,
bubbles: true,
});
window.dispatchEvent(event);
});
expect(result.current.currentEntry?.state).toEqual({ count: 2 });
});
});