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:
Jan-Henrik Bruhn 2025-11-10 12:26:34 +01:00
parent 650819a083
commit 7fa0965001
9 changed files with 5 additions and 365 deletions

View file

@ -3,7 +3,7 @@ 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';
import { resetWorkspaceStore } from '../../test-utils/test-helpers';
/**
* Integration tests for store synchronization patterns

View file

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

View file

@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
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 { Timeline } from '../types/timeline';

View file

@ -5,7 +5,7 @@ import {
loadDocumentFromStorage,
clearWorkspaceStorage,
} from './workspace/persistence';
import { mockNodeTypes, mockEdgeTypes } from '../test/mocks';
import { mockNodeTypes, mockEdgeTypes } from '../test-utils/mocks';
// Create a mock showToast that we can track
const mockShowToast = vi.fn();

View file

@ -12,13 +12,13 @@ export default defineConfig({
test: {
globals: true,
environment: "happy-dom",
setupFiles: "./src/test/setup.ts",
setupFiles: "./src/test-utils/setup.ts",
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: [
"node_modules/",
"src/test/",
"src/test-utils/",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",