glam/frontend/tests/unit/ui-state.test.ts
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

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;
});
});
});