- 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.
333 lines
9.8 KiB
TypeScript
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 });
|
|
});
|
|
});
|