mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +00:00
there is a __tests__ folder and a test folder. That seems confusing, is that a SotA TypeScript/REact/Vite testing pattern? (vibe-kanban c7c8b21b)
This commit is contained in:
parent
650819a083
commit
7fa0965001
9 changed files with 5 additions and 365 deletions
|
|
@ -3,7 +3,7 @@ import { useWorkspaceStore } from '../../stores/workspaceStore';
|
||||||
import { useGraphStore } from '../../stores/graphStore';
|
import { useGraphStore } from '../../stores/graphStore';
|
||||||
import { useTimelineStore } from '../../stores/timelineStore';
|
import { useTimelineStore } from '../../stores/timelineStore';
|
||||||
import { useHistoryStore } from '../../stores/historyStore';
|
import { useHistoryStore } from '../../stores/historyStore';
|
||||||
import { resetWorkspaceStore } from '../../test/test-helpers';
|
import { resetWorkspaceStore } from '../../test-utils/test-helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration tests for store synchronization patterns
|
* Integration tests for store synchronization patterns
|
||||||
|
|
|
||||||
|
|
@ -1,360 +0,0 @@
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
||||||
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
|
||||||
import { useGraphStore } from '../../stores/graphStore';
|
|
||||||
import { useTimelineStore } from '../../stores/timelineStore';
|
|
||||||
import { useHistoryStore } from '../../stores/historyStore';
|
|
||||||
import { resetWorkspaceStore } from '../../test/test-helpers';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integration tests for store synchronization patterns
|
|
||||||
*
|
|
||||||
* These tests verify that stores properly integrate through their documented APIs,
|
|
||||||
* NOT through UI interactions. They test the business logic layer - how stores
|
|
||||||
* communicate and stay synchronized.
|
|
||||||
*
|
|
||||||
* Key integration points tested:
|
|
||||||
* - Document creation initializes all related stores (timeline, history, graph types)
|
|
||||||
* - Document persistence includes data from all stores
|
|
||||||
* - Store state is properly isolated per document
|
|
||||||
*/
|
|
||||||
describe('Store Synchronization Integration', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
localStorage.clear();
|
|
||||||
resetWorkspaceStore();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Document Creation Integration', () => {
|
|
||||||
it('should initialize timeline store when creating document', () => {
|
|
||||||
const docId = useWorkspaceStore.getState().createDocument('Timeline Test');
|
|
||||||
|
|
||||||
// Timeline should be loaded for this document
|
|
||||||
const timelineStore = useTimelineStore.getState();
|
|
||||||
expect(timelineStore.currentState).toBeDefined();
|
|
||||||
expect(timelineStore.states.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should persist node and edge types to document', () => {
|
|
||||||
const docId = useWorkspaceStore.getState().createDocument('Types Test');
|
|
||||||
|
|
||||||
// Get fresh state after creation
|
|
||||||
const doc = useWorkspaceStore.getState().documents.get(docId);
|
|
||||||
expect(doc).toBeDefined();
|
|
||||||
expect(doc?.nodeTypes).toBeDefined();
|
|
||||||
expect(doc?.nodeTypes.length).toBeGreaterThan(0);
|
|
||||||
expect(doc?.edgeTypes).toBeDefined();
|
|
||||||
expect(doc?.edgeTypes.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create empty initial timeline state', () => {
|
|
||||||
const docId = useWorkspaceStore.getState().createDocument('Initial State Test');
|
|
||||||
|
|
||||||
// Get fresh state after creation
|
|
||||||
const doc = useWorkspaceStore.getState().documents.get(docId);
|
|
||||||
expect(doc?.timeline).toBeDefined();
|
|
||||||
expect(doc?.timeline.states).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Document Persistence Integration', () => {
|
|
||||||
it('should save document to localStorage with all data', () => {
|
|
||||||
const docId = useWorkspaceStore.getState().createDocument('Persistence Test');
|
|
||||||
|
|
||||||
// Document is already saved during creation, check localStorage
|
|
||||||
const stored = localStorage.getItem(`document-${docId}`);
|
|
||||||
expect(stored).toBeTruthy();
|
|
||||||
|
|
||||||
const storedData = JSON.parse(stored!);
|
|
||||||
expect(storedData.metadata.title).toBe('Persistence Test');
|
|
||||||
expect(storedData.nodeTypes).toBeDefined();
|
|
||||||
expect(storedData.edgeTypes).toBeDefined();
|
|
||||||
expect(storedData.timeline).toBeDefined();
|
|
||||||
expect(storedData.bibliography).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should save workspace state to localStorage', () => {
|
|
||||||
const doc1Id = useWorkspaceStore.getState().createDocument('Doc 1');
|
|
||||||
const doc2Id = useWorkspaceStore.getState().createDocument('Doc 2');
|
|
||||||
|
|
||||||
// Workspace state should be in localStorage
|
|
||||||
const workspaceState = localStorage.getItem('workspace-state');
|
|
||||||
expect(workspaceState).toBeTruthy();
|
|
||||||
|
|
||||||
const state = JSON.parse(workspaceState!);
|
|
||||||
expect(state.documentOrder).toContain(doc1Id);
|
|
||||||
expect(state.documentOrder).toContain(doc2Id);
|
|
||||||
expect(state.activeDocumentId).toBe(doc2Id); // Last created is active
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Document Isolation', () => {
|
|
||||||
it('should maintain separate documents in workspace', () => {
|
|
||||||
const doc1Id = useWorkspaceStore.getState().createDocument('Document 1');
|
|
||||||
const doc2Id = useWorkspaceStore.getState().createDocument('Document 2');
|
|
||||||
const doc3Id = useWorkspaceStore.getState().createDocument('Document 3');
|
|
||||||
|
|
||||||
// Get fresh state
|
|
||||||
const state = useWorkspaceStore.getState();
|
|
||||||
|
|
||||||
// All documents should exist in workspace
|
|
||||||
expect(state.documents.size).toBe(3);
|
|
||||||
expect(state.documents.has(doc1Id)).toBe(true);
|
|
||||||
expect(state.documents.has(doc2Id)).toBe(true);
|
|
||||||
expect(state.documents.has(doc3Id)).toBe(true);
|
|
||||||
|
|
||||||
// Document order should be correct
|
|
||||||
expect(state.documentOrder).toEqual([doc1Id, doc2Id, doc3Id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should isolate timeline states per document', async () => {
|
|
||||||
const doc1Id = useWorkspaceStore.getState().createDocument('Doc 1');
|
|
||||||
const timelineStore1 = useTimelineStore.getState();
|
|
||||||
const doc1StateCount = timelineStore1.states.length;
|
|
||||||
|
|
||||||
const doc2Id = useWorkspaceStore.getState().createDocument('Doc 2');
|
|
||||||
const timelineStore2 = useTimelineStore.getState();
|
|
||||||
const doc2StateCount = timelineStore2.states.length;
|
|
||||||
|
|
||||||
// Each document should have its own timeline
|
|
||||||
// (They may have the same count if both start with default state)
|
|
||||||
expect(doc1StateCount).toBeGreaterThan(0);
|
|
||||||
expect(doc2StateCount).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Document Deletion Integration', () => {
|
|
||||||
it('should remove document from workspace and storage', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
|
|
||||||
const doc1Id = workspaceStore.createDocument('Keep');
|
|
||||||
const doc2Id = workspaceStore.createDocument('Delete');
|
|
||||||
|
|
||||||
// Mock confirm for deletion
|
|
||||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
||||||
|
|
||||||
workspaceStore.deleteDocument(doc2Id);
|
|
||||||
|
|
||||||
// Document should be removed from workspace
|
|
||||||
expect(workspaceStore.documents.has(doc2Id)).toBe(false);
|
|
||||||
expect(workspaceStore.documentOrder).not.toContain(doc2Id);
|
|
||||||
|
|
||||||
// Document should be removed from localStorage
|
|
||||||
const stored = localStorage.getItem(`document-${doc2Id}`);
|
|
||||||
expect(stored).toBeNull();
|
|
||||||
|
|
||||||
// Other document should still exist
|
|
||||||
expect(workspaceStore.documents.has(doc1Id)).toBe(true);
|
|
||||||
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update active document when deleting current document', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
|
|
||||||
const doc1Id = workspaceStore.createDocument('Doc 1');
|
|
||||||
const doc2Id = workspaceStore.createDocument('Doc 2');
|
|
||||||
|
|
||||||
expect(workspaceStore.activeDocumentId).toBe(doc2Id);
|
|
||||||
|
|
||||||
// Mock confirm
|
|
||||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
||||||
|
|
||||||
workspaceStore.deleteDocument(doc2Id);
|
|
||||||
|
|
||||||
// Active document should switch to doc1
|
|
||||||
const state = useWorkspaceStore.getState();
|
|
||||||
expect(state.activeDocumentId).toBe(doc1Id);
|
|
||||||
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Document Type Management Integration', () => {
|
|
||||||
it('should allow adding custom node types to document', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
const docId = workspaceStore.createDocument('Custom Types');
|
|
||||||
|
|
||||||
const customType = {
|
|
||||||
id: 'custom-type',
|
|
||||||
label: 'Custom Type',
|
|
||||||
color: '#ff0000',
|
|
||||||
shape: 'diamond' as const,
|
|
||||||
icon: 'Star',
|
|
||||||
description: 'A custom node type',
|
|
||||||
};
|
|
||||||
|
|
||||||
workspaceStore.addNodeTypeToDocument(docId, customType);
|
|
||||||
|
|
||||||
const doc = workspaceStore.documents.get(docId);
|
|
||||||
const hasCustomType = doc?.nodeTypes.some(t => t.id === 'custom-type');
|
|
||||||
expect(hasCustomType).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow updating edge types in document', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
const docId = workspaceStore.createDocument('Edge Types');
|
|
||||||
|
|
||||||
const doc = workspaceStore.documents.get(docId);
|
|
||||||
const firstEdgeType = doc?.edgeTypes[0];
|
|
||||||
expect(firstEdgeType).toBeDefined();
|
|
||||||
|
|
||||||
workspaceStore.updateEdgeTypeInDocument(docId, firstEdgeType!.id, {
|
|
||||||
label: 'Updated Label',
|
|
||||||
color: '#00ff00',
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedDoc = workspaceStore.documents.get(docId);
|
|
||||||
const updatedType = updatedDoc?.edgeTypes.find(t => t.id === firstEdgeType!.id);
|
|
||||||
expect(updatedType?.label).toBe('Updated Label');
|
|
||||||
expect(updatedType?.color).toBe('#00ff00');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Document Duplication Integration', () => {
|
|
||||||
it('should duplicate document with all its data', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
const originalId = workspaceStore.createDocument('Original');
|
|
||||||
|
|
||||||
// Add custom type to original
|
|
||||||
workspaceStore.addNodeTypeToDocument(originalId, {
|
|
||||||
id: 'custom',
|
|
||||||
label: 'Custom',
|
|
||||||
color: '#ff0000',
|
|
||||||
shape: 'circle' as const,
|
|
||||||
icon: 'Star',
|
|
||||||
description: 'Custom type',
|
|
||||||
});
|
|
||||||
|
|
||||||
const duplicateId = workspaceStore.duplicateDocument(originalId);
|
|
||||||
expect(duplicateId).toBeTruthy();
|
|
||||||
expect(duplicateId).not.toBe(originalId);
|
|
||||||
|
|
||||||
// Duplicate should have the custom type
|
|
||||||
const duplicateDoc = workspaceStore.documents.get(duplicateId);
|
|
||||||
const hasCustomType = duplicateDoc?.nodeTypes.some(t => t.id === 'custom');
|
|
||||||
expect(hasCustomType).toBe(true);
|
|
||||||
|
|
||||||
// Both documents should exist
|
|
||||||
expect(workspaceStore.documents.size).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('History Store Integration', () => {
|
|
||||||
it('should initialize history for new document', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
const docId = workspaceStore.createDocument('History Test');
|
|
||||||
|
|
||||||
const historyStore = useHistoryStore.getState();
|
|
||||||
|
|
||||||
// History should be initialized (can check if functions work)
|
|
||||||
const canUndo = historyStore.canUndo(docId);
|
|
||||||
const canRedo = historyStore.canRedo(docId);
|
|
||||||
|
|
||||||
// New document shouldn't have undo/redo yet
|
|
||||||
expect(canUndo).toBe(false);
|
|
||||||
expect(canRedo).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should maintain separate history per document', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
const doc1Id = workspaceStore.createDocument('Doc 1');
|
|
||||||
const doc2Id = workspaceStore.createDocument('Doc 2');
|
|
||||||
|
|
||||||
const historyStore = useHistoryStore.getState();
|
|
||||||
|
|
||||||
// Both documents should have independent history
|
|
||||||
const doc1CanUndo = historyStore.canUndo(doc1Id);
|
|
||||||
const doc2CanUndo = historyStore.canUndo(doc2Id);
|
|
||||||
|
|
||||||
// Both should be false for new documents
|
|
||||||
expect(doc1CanUndo).toBe(false);
|
|
||||||
expect(doc2CanUndo).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove history when document is deleted', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
const docId = workspaceStore.createDocument('To Delete');
|
|
||||||
|
|
||||||
const historyStore = useHistoryStore.getState();
|
|
||||||
|
|
||||||
// Initialize history
|
|
||||||
historyStore.initializeHistory(docId);
|
|
||||||
|
|
||||||
// Mock confirm
|
|
||||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
||||||
|
|
||||||
// Delete document
|
|
||||||
workspaceStore.deleteDocument(docId);
|
|
||||||
|
|
||||||
// History should be removed (checking canUndo shouldn't error)
|
|
||||||
const canUndo = historyStore.canUndo(docId);
|
|
||||||
expect(canUndo).toBe(false);
|
|
||||||
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Document Rename Integration', () => {
|
|
||||||
it('should update document title and metadata', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
const docId = workspaceStore.createDocument('Original Title');
|
|
||||||
|
|
||||||
workspaceStore.renameDocument(docId, 'New Title');
|
|
||||||
|
|
||||||
const doc = workspaceStore.documents.get(docId);
|
|
||||||
expect(doc?.metadata.title).toBe('New Title');
|
|
||||||
|
|
||||||
const metadata = workspaceStore.documentMetadata.get(docId);
|
|
||||||
expect(metadata?.title).toBe('New Title');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should mark document as dirty after rename', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
const docId = workspaceStore.createDocument('Test');
|
|
||||||
|
|
||||||
workspaceStore.renameDocument(docId, 'Renamed');
|
|
||||||
|
|
||||||
const metadata = workspaceStore.documentMetadata.get(docId);
|
|
||||||
expect(metadata?.isDirty).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Multiple Document Operations', () => {
|
|
||||||
it('should handle creating many documents', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
|
|
||||||
const docIds: string[] = [];
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
const docId = workspaceStore.createDocument(`Document ${i}`);
|
|
||||||
docIds.push(docId);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(workspaceStore.documents.size).toBe(10);
|
|
||||||
expect(workspaceStore.documentOrder).toHaveLength(10);
|
|
||||||
|
|
||||||
// All should be unique
|
|
||||||
const uniqueIds = new Set(docIds);
|
|
||||||
expect(uniqueIds.size).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle deleting multiple documents in sequence', () => {
|
|
||||||
const workspaceStore = useWorkspaceStore.getState();
|
|
||||||
|
|
||||||
const doc1Id = workspaceStore.createDocument('Doc 1');
|
|
||||||
const doc2Id = workspaceStore.createDocument('Doc 2');
|
|
||||||
const doc3Id = workspaceStore.createDocument('Doc 3');
|
|
||||||
|
|
||||||
// Mock confirm
|
|
||||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
||||||
|
|
||||||
workspaceStore.deleteDocument(doc2Id);
|
|
||||||
expect(workspaceStore.documents.size).toBe(2);
|
|
||||||
|
|
||||||
workspaceStore.deleteDocument(doc3Id);
|
|
||||||
expect(workspaceStore.documents.size).toBe(1);
|
|
||||||
|
|
||||||
expect(workspaceStore.documents.has(doc1Id)).toBe(true);
|
|
||||||
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { useHistoryStore, type DocumentSnapshot, type HistoryAction } from './historyStore';
|
import { useHistoryStore, type DocumentSnapshot, type HistoryAction } from './historyStore';
|
||||||
import { mockNodeTypes, mockEdgeTypes, mockLabels } from '../test/mocks';
|
import { mockNodeTypes, mockEdgeTypes, mockLabels } from '../test-utils/mocks';
|
||||||
import type { ConstellationDocument } from './persistence/types';
|
import type { ConstellationDocument } from './persistence/types';
|
||||||
import type { Timeline } from '../types/timeline';
|
import type { Timeline } from '../types/timeline';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
loadDocumentFromStorage,
|
loadDocumentFromStorage,
|
||||||
clearWorkspaceStorage,
|
clearWorkspaceStorage,
|
||||||
} from './workspace/persistence';
|
} from './workspace/persistence';
|
||||||
import { mockNodeTypes, mockEdgeTypes } from '../test/mocks';
|
import { mockNodeTypes, mockEdgeTypes } from '../test-utils/mocks';
|
||||||
|
|
||||||
// Create a mock showToast that we can track
|
// Create a mock showToast that we can track
|
||||||
const mockShowToast = vi.fn();
|
const mockShowToast = vi.fn();
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@ export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "happy-dom",
|
environment: "happy-dom",
|
||||||
setupFiles: "./src/test/setup.ts",
|
setupFiles: "./src/test-utils/setup.ts",
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: "v8",
|
provider: "v8",
|
||||||
reporter: ["text", "json", "html"],
|
reporter: ["text", "json", "html"],
|
||||||
exclude: [
|
exclude: [
|
||||||
"node_modules/",
|
"node_modules/",
|
||||||
"src/test/",
|
"src/test-utils/",
|
||||||
"**/*.test.ts",
|
"**/*.test.ts",
|
||||||
"**/*.test.tsx",
|
"**/*.test.tsx",
|
||||||
"**/*.spec.ts",
|
"**/*.spec.ts",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue