- 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.
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
/**
|
|
* UI State Persistence Tests
|
|
*
|
|
* Tests for localStorage utilities and UIStateContext
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
loadUIState,
|
|
saveUIState,
|
|
updateUIState,
|
|
clearUIState,
|
|
addRecentFile,
|
|
addRecentQuery,
|
|
clearRecentFiles,
|
|
clearRecentQueries,
|
|
exportUIState,
|
|
importUIState,
|
|
DEFAULT_UI_STATE,
|
|
UIState,
|
|
} from '../../src/lib/storage/ui-state';
|
|
|
|
describe('UI State localStorage Utilities', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
localStorage.clear();
|
|
});
|
|
|
|
describe('loadUIState', () => {
|
|
it('should return default state when localStorage is empty', () => {
|
|
const state = loadUIState();
|
|
expect(state).toEqual(DEFAULT_UI_STATE);
|
|
});
|
|
|
|
it('should load saved state from localStorage', () => {
|
|
const customState: UIState = {
|
|
...DEFAULT_UI_STATE,
|
|
theme: 'dark',
|
|
visualization: {
|
|
...DEFAULT_UI_STATE.visualization,
|
|
layout: 'hierarchical',
|
|
nodeSize: 12,
|
|
},
|
|
};
|
|
|
|
localStorage.setItem('rdf-visualizer:ui-state', JSON.stringify(customState));
|
|
|
|
const loaded = loadUIState();
|
|
expect(loaded.theme).toBe('dark');
|
|
expect(loaded.visualization.layout).toBe('hierarchical');
|
|
expect(loaded.visualization.nodeSize).toBe(12);
|
|
});
|
|
|
|
it('should return default state on parse error', () => {
|
|
localStorage.setItem('rdf-visualizer:ui-state', 'invalid json');
|
|
|
|
const state = loadUIState();
|
|
expect(state).toEqual(DEFAULT_UI_STATE);
|
|
});
|
|
|
|
it('should migrate old version to current version', () => {
|
|
const oldState = {
|
|
...DEFAULT_UI_STATE,
|
|
version: 0, // Old version
|
|
theme: 'dark',
|
|
};
|
|
|
|
localStorage.setItem('rdf-visualizer:ui-state', JSON.stringify(oldState));
|
|
|
|
const loaded = loadUIState();
|
|
expect(loaded.version).toBe(1); // Current version
|
|
expect(loaded.theme).toBe('dark'); // Preserved data
|
|
});
|
|
});
|
|
|
|
describe('saveUIState', () => {
|
|
it('should save state to localStorage', () => {
|
|
const state: UIState = {
|
|
...DEFAULT_UI_STATE,
|
|
theme: 'dark',
|
|
};
|
|
|
|
const success = saveUIState(state);
|
|
expect(success).toBe(true);
|
|
|
|
const stored = localStorage.getItem('rdf-visualizer:ui-state');
|
|
expect(stored).toBeTruthy();
|
|
|
|
const parsed = JSON.parse(stored!);
|
|
expect(parsed.theme).toBe('dark');
|
|
});
|
|
|
|
it('should update last-updated timestamp', () => {
|
|
const state = { ...DEFAULT_UI_STATE };
|
|
saveUIState(state);
|
|
|
|
const timestamp = localStorage.getItem('rdf-visualizer:last-updated');
|
|
expect(timestamp).toBeTruthy();
|
|
expect(parseInt(timestamp!)).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('updateUIState', () => {
|
|
it('should update partial state', () => {
|
|
// Save initial state
|
|
saveUIState(DEFAULT_UI_STATE);
|
|
|
|
// Update theme only
|
|
const success = updateUIState({ theme: 'dark' });
|
|
expect(success).toBe(true);
|
|
|
|
// Verify update
|
|
const loaded = loadUIState();
|
|
expect(loaded.theme).toBe('dark');
|
|
expect(loaded.visualization).toEqual(DEFAULT_UI_STATE.visualization); // Unchanged
|
|
});
|
|
|
|
it('should deep merge nested objects', () => {
|
|
saveUIState(DEFAULT_UI_STATE);
|
|
|
|
// Update only visualization.nodeSize
|
|
updateUIState({
|
|
visualization: {
|
|
...DEFAULT_UI_STATE.visualization,
|
|
nodeSize: 15,
|
|
},
|
|
});
|
|
|
|
const loaded = loadUIState();
|
|
expect(loaded.visualization.nodeSize).toBe(15);
|
|
expect(loaded.visualization.layout).toBe(DEFAULT_UI_STATE.visualization.layout); // Unchanged
|
|
});
|
|
});
|
|
|
|
describe('clearUIState', () => {
|
|
it('should remove state from localStorage', () => {
|
|
saveUIState(DEFAULT_UI_STATE);
|
|
expect(localStorage.getItem('rdf-visualizer:ui-state')).toBeTruthy();
|
|
|
|
const success = clearUIState();
|
|
expect(success).toBe(true);
|
|
expect(localStorage.getItem('rdf-visualizer:ui-state')).toBeNull();
|
|
});
|
|
|
|
it('should remove last-updated timestamp', () => {
|
|
saveUIState(DEFAULT_UI_STATE);
|
|
expect(localStorage.getItem('rdf-visualizer:last-updated')).toBeTruthy();
|
|
|
|
clearUIState();
|
|
expect(localStorage.getItem('rdf-visualizer:last-updated')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('addRecentFile', () => {
|
|
it('should add file to recent files', () => {
|
|
const file = {
|
|
id: 'file-1',
|
|
name: 'test.ttl',
|
|
format: 'turtle',
|
|
};
|
|
|
|
addRecentFile(file);
|
|
|
|
const state = loadUIState();
|
|
expect(state.recentFiles).toHaveLength(1);
|
|
expect(state.recentFiles[0].id).toBe('file-1');
|
|
expect(state.recentFiles[0].name).toBe('test.ttl');
|
|
expect(state.recentFiles[0].timestamp).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should add file to front of list', () => {
|
|
addRecentFile({ id: 'file-1', name: 'first.ttl', format: 'turtle' });
|
|
addRecentFile({ id: 'file-2', name: 'second.ttl', format: 'turtle' });
|
|
|
|
const state = loadUIState();
|
|
expect(state.recentFiles[0].id).toBe('file-2'); // Most recent first
|
|
expect(state.recentFiles[1].id).toBe('file-1');
|
|
});
|
|
|
|
it('should remove duplicate files', () => {
|
|
addRecentFile({ id: 'file-1', name: 'test.ttl', format: 'turtle' });
|
|
addRecentFile({ id: 'file-2', name: 'other.ttl', format: 'turtle' });
|
|
addRecentFile({ id: 'file-1', name: 'test.ttl', format: 'turtle' }); // Duplicate
|
|
|
|
const state = loadUIState();
|
|
expect(state.recentFiles).toHaveLength(2);
|
|
expect(state.recentFiles[0].id).toBe('file-1'); // Moved to front
|
|
});
|
|
|
|
it('should limit to 10 recent files', () => {
|
|
// Add 15 files
|
|
for (let i = 0; i < 15; i++) {
|
|
addRecentFile({ id: `file-${i}`, name: `file${i}.ttl`, format: 'turtle' });
|
|
}
|
|
|
|
const state = loadUIState();
|
|
expect(state.recentFiles).toHaveLength(10); // Max 10
|
|
expect(state.recentFiles[0].id).toBe('file-14'); // Most recent
|
|
expect(state.recentFiles[9].id).toBe('file-5'); // 10th most recent
|
|
});
|
|
});
|
|
|
|
describe('addRecentQuery', () => {
|
|
it('should add query to recent queries', () => {
|
|
addRecentQuery('SELECT * WHERE { ?s ?p ?o }');
|
|
|
|
const state = loadUIState();
|
|
expect(state.recentQueries).toHaveLength(1);
|
|
expect(state.recentQueries[0].query).toBe('SELECT * WHERE { ?s ?p ?o }');
|
|
});
|
|
|
|
it('should remove duplicate queries', () => {
|
|
addRecentQuery('SELECT * WHERE { ?s ?p ?o }');
|
|
addRecentQuery('SELECT ?s WHERE { ?s a ?type }');
|
|
addRecentQuery('SELECT * WHERE { ?s ?p ?o }'); // Duplicate
|
|
|
|
const state = loadUIState();
|
|
expect(state.recentQueries).toHaveLength(2);
|
|
expect(state.recentQueries[0].query).toBe('SELECT * WHERE { ?s ?p ?o }'); // Moved to front
|
|
});
|
|
|
|
it('should limit to 20 recent queries', () => {
|
|
for (let i = 0; i < 25; i++) {
|
|
addRecentQuery(`SELECT * WHERE { ?s ?p ?o${i} }`);
|
|
}
|
|
|
|
const state = loadUIState();
|
|
expect(state.recentQueries).toHaveLength(20); // Max 20
|
|
});
|
|
});
|
|
|
|
describe('clearRecentFiles', () => {
|
|
it('should clear recent files', () => {
|
|
addRecentFile({ id: 'file-1', name: 'test.ttl', format: 'turtle' });
|
|
addRecentFile({ id: 'file-2', name: 'other.ttl', format: 'turtle' });
|
|
|
|
clearRecentFiles();
|
|
|
|
const state = loadUIState();
|
|
expect(state.recentFiles).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('clearRecentQueries', () => {
|
|
it('should clear recent queries', () => {
|
|
addRecentQuery('SELECT * WHERE { ?s ?p ?o }');
|
|
addRecentQuery('SELECT ?s WHERE { ?s a ?type }');
|
|
|
|
clearRecentQueries();
|
|
|
|
const state = loadUIState();
|
|
expect(state.recentQueries).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('exportUIState', () => {
|
|
it('should export state as JSON string', () => {
|
|
saveUIState({ ...DEFAULT_UI_STATE, theme: 'dark' });
|
|
|
|
const json = exportUIState();
|
|
expect(json).toBeTruthy();
|
|
|
|
const parsed = JSON.parse(json);
|
|
expect(parsed.theme).toBe('dark');
|
|
});
|
|
|
|
it('should export prettified JSON', () => {
|
|
saveUIState(DEFAULT_UI_STATE);
|
|
|
|
const json = exportUIState();
|
|
expect(json).toContain('\n'); // Contains newlines (prettified)
|
|
expect(json).toContain(' '); // Contains indentation
|
|
});
|
|
});
|
|
|
|
describe('importUIState', () => {
|
|
it('should import valid JSON state', () => {
|
|
const exportedState = {
|
|
...DEFAULT_UI_STATE,
|
|
theme: 'dark' as const,
|
|
visualization: {
|
|
...DEFAULT_UI_STATE.visualization,
|
|
nodeSize: 15,
|
|
},
|
|
};
|
|
|
|
const json = JSON.stringify(exportedState);
|
|
const success = importUIState(json);
|
|
expect(success).toBe(true);
|
|
|
|
const loaded = loadUIState();
|
|
expect(loaded.theme).toBe('dark');
|
|
expect(loaded.visualization.nodeSize).toBe(15);
|
|
});
|
|
|
|
it('should reject invalid JSON', () => {
|
|
const success = importUIState('invalid json');
|
|
expect(success).toBe(false);
|
|
});
|
|
|
|
it('should reject invalid state structure', () => {
|
|
const invalidState = { foo: 'bar' }; // Missing required fields
|
|
const success = importUIState(JSON.stringify(invalidState));
|
|
expect(success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('localStorage unavailable scenarios', () => {
|
|
it('should handle localStorage errors gracefully', () => {
|
|
// Save original setItem
|
|
const originalSetItem = Storage.prototype.setItem;
|
|
|
|
// Override with throwing version
|
|
Storage.prototype.setItem = function() {
|
|
throw new Error('QuotaExceededError');
|
|
};
|
|
|
|
const success = saveUIState(DEFAULT_UI_STATE);
|
|
expect(success).toBe(false);
|
|
|
|
// Restore original
|
|
Storage.prototype.setItem = originalSetItem;
|
|
});
|
|
|
|
it('should return default state when localStorage throws on getItem', () => {
|
|
// Save original getItem
|
|
const originalGetItem = Storage.prototype.getItem;
|
|
|
|
// Override with throwing version
|
|
Storage.prototype.getItem = function() {
|
|
throw new Error('SecurityError');
|
|
};
|
|
|
|
const state = loadUIState();
|
|
expect(state).toEqual(DEFAULT_UI_STATE);
|
|
|
|
// Restore original
|
|
Storage.prototype.getItem = originalGetItem;
|
|
});
|
|
});
|
|
});
|