import { describe, it, expect, beforeEach, vi } from 'vitest'; import type { CSLReference } from '../types/bibliography'; // Mock citation.js with factory function to avoid hoisting issues vi.mock('@citation-js/core', () => { const mockCiteData: CSLReference[] = []; const createMockCiteInstance = () => ({ data: mockCiteData, add: vi.fn((refs: CSLReference | CSLReference[]) => { const refsArray = Array.isArray(refs) ? refs : [refs]; mockCiteData.push(...refsArray); }), set: vi.fn((refs: CSLReference[]) => { mockCiteData.length = 0; mockCiteData.push(...refs); }), reset: vi.fn(() => { mockCiteData.length = 0; }), format: vi.fn((type: string, options?: { template?: string; format?: string; lang?: string }) => { if (type === 'bibliography') { const format = options?.format || 'html'; if (mockCiteData.length === 0) return ''; const citations = mockCiteData.map(ref => { const authors = ref.author?.map(a => a.literal || `${a.family}, ${a.given}` ).join(', ') || 'Unknown Author'; const year = ref.issued?.['date-parts']?.[0]?.[0] || 'n.d.'; const title = ref.title || 'Untitled'; if (format === 'html') { return `
${authors} (${year}). ${title}.
`; } else { return `${authors} (${year}). ${title}.`; } }).join('\n'); return citations; } if (type === 'bibtex') { return mockCiteData.map(ref => `@article{${ref.id},\n title={${ref.title}}\n}` ).join('\n\n'); } if (type === 'ris') { return mockCiteData.map(ref => `TY - JOUR\nID - ${ref.id}\nTI - ${ref.title}\nER -` ).join('\n\n'); } return ''; }), }); interface MockCiteConstructor { new (data?: CSLReference[]): ReturnType; async: (input: string) => Promise>; } const MockCiteClass = vi.fn(() => createMockCiteInstance()) as unknown as MockCiteConstructor; MockCiteClass.async = vi.fn(async (input: string) => { // Simulate parsing different input formats if (input.startsWith('10.')) { // DOI const instance = createMockCiteInstance(); instance.data = [{ id: 'doi-ref', type: 'article-journal' as const, title: 'Article from DOI', DOI: input, }]; return instance; } else if (input.startsWith('@')) { // BibTeX const instance = createMockCiteInstance(); instance.data = [{ id: 'bibtex-ref', type: 'article-journal' as const, title: 'Article from BibTeX', }]; return instance; } else if (input.includes('http')) { // URL const instance = createMockCiteInstance(); instance.data = [{ id: 'url-ref', type: 'webpage' as const, title: 'Webpage from URL', URL: input, }]; return instance; } throw new Error('Could not parse citation data'); }); return { Cite: MockCiteClass, }; }); // Mock the plugin imports vi.mock('@citation-js/plugin-csl', () => ({})); vi.mock('@citation-js/plugin-doi', () => ({})); vi.mock('@citation-js/plugin-bibtex', () => ({})); vi.mock('@citation-js/plugin-ris', () => ({})); vi.mock('@citation-js/plugin-software-formats', () => ({})); // Import the store after mocks are set up import { useBibliographyStore, clearBibliographyForDocumentSwitch } from './bibliographyStore'; // Helper functions function createMockReference(id: string, overrides?: Partial): CSLReference { return { id, type: 'article-journal', title: `Test Article ${id}`, author: [{ family: 'Doe', given: 'John' }], issued: { 'date-parts': [[2024]] }, 'container-title': 'Test Journal', ...overrides, }; } describe('bibliographyStore', () => { beforeEach(() => { // Reset store with new mock Cite instance const mockInstance = { data: [], add: vi.fn((refs: CSLReference | CSLReference[]) => { const refsArray = Array.isArray(refs) ? refs : [refs]; (mockInstance.data as CSLReference[]).push(...refsArray); }), set: vi.fn((refs: CSLReference[]) => { (mockInstance.data as CSLReference[]).length = 0; (mockInstance.data as CSLReference[]).push(...refs); }), reset: vi.fn(() => { (mockInstance.data as CSLReference[]).length = 0; }), format: vi.fn(() => ''), }; useBibliographyStore.setState({ citeInstance: mockInstance as never, appMetadata: {}, settings: { defaultStyle: 'apa', sortOrder: 'author', }, }); // Clear all mocks vi.clearAllMocks(); }); describe('Initial State', () => { it('should start with empty references', () => { const { getReferences } = useBibliographyStore.getState(); expect(getReferences()).toHaveLength(0); }); it('should have default settings', () => { const { settings } = useBibliographyStore.getState(); expect(settings.defaultStyle).toBe('apa'); expect(settings.sortOrder).toBe('author'); }); it('should have empty metadata', () => { const { appMetadata } = useBibliographyStore.getState(); expect(Object.keys(appMetadata)).toHaveLength(0); }); }); describe('Add Reference', () => { it('should add a new reference', () => { const { addReference, getReferences } = useBibliographyStore.getState(); const ref = createMockReference('ref-1'); const id = addReference(ref); expect(id).toBe('ref-1'); expect(getReferences()).toHaveLength(1); expect(getReferences()[0].id).toBe('ref-1'); }); it('should generate ID if not provided', () => { const { addReference, getReferences } = useBibliographyStore.getState(); const ref: Partial = { type: 'article-journal', title: 'Test Article', author: [{ family: 'Doe', given: 'John' }], }; const id = addReference(ref); expect(id).toMatch(/^ref-\d+-[a-z0-9]+$/); expect(getReferences()).toHaveLength(1); }); it('should create app metadata for new reference', () => { const { addReference } = useBibliographyStore.getState(); const id = addReference(createMockReference('ref-1')); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata[id]).toBeDefined(); expect(appMetadata[id].id).toBe(id); expect(appMetadata[id].tags).toEqual([]); expect(appMetadata[id].createdAt).toBeTruthy(); expect(appMetadata[id].updatedAt).toBeTruthy(); }); it('should set createdAt and updatedAt timestamps', () => { const { addReference } = useBibliographyStore.getState(); const id = addReference(createMockReference('ref-1')); const { appMetadata } = useBibliographyStore.getState(); const metadata = appMetadata[id]; expect(metadata.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); expect(metadata.updatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); }); }); describe('Update Reference', () => { let refId: string; beforeEach(() => { const { addReference } = useBibliographyStore.getState(); refId = addReference(createMockReference('ref-1')); }); it('should update reference data', () => { const { updateReference, getReferenceById } = useBibliographyStore.getState(); updateReference(refId, { title: 'Updated Title' }); const updated = getReferenceById(refId); expect(updated?.title).toBe('Updated Title'); }); it('should persist the update in citeInstance data', () => { const { updateReference, getCSLData } = useBibliographyStore.getState(); updateReference(refId, { title: 'Updated Title' }); const cslData = getCSLData(); expect(cslData.find(r => r.id === refId)?.title).toBe('Updated Title'); }); it('should update metadata timestamp', async () => { const { updateReference, appMetadata } = useBibliographyStore.getState(); const originalTime = appMetadata[refId].updatedAt; // Wait to ensure different timestamp await new Promise(resolve => setTimeout(resolve, 10)); updateReference(refId, { title: 'Updated Title' }); const newTime = useBibliographyStore.getState().appMetadata[refId].updatedAt; expect(newTime).not.toBe(originalTime); }); it('should not affect other references', () => { const { addReference, updateReference, getReferenceById } = useBibliographyStore.getState(); const ref2Id = addReference(createMockReference('ref-2')); updateReference(refId, { title: 'Updated Title' }); const ref2 = getReferenceById(ref2Id); expect(ref2?.title).toBe('Test Article ref-2'); }); }); describe('Delete Reference', () => { let refId: string; beforeEach(() => { const { addReference } = useBibliographyStore.getState(); refId = addReference(createMockReference('ref-1')); }); it('should delete a reference', () => { const { deleteReference, getReferences } = useBibliographyStore.getState(); deleteReference(refId); expect(getReferences()).toHaveLength(0); }); it('should remove metadata', () => { const { deleteReference } = useBibliographyStore.getState(); deleteReference(refId); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata[refId]).toBeUndefined(); }); it('should not affect other references', () => { const { addReference, deleteReference, getReferences } = useBibliographyStore.getState(); const ref2Id = addReference(createMockReference('ref-2')); deleteReference(refId); const remaining = getReferences(); expect(remaining).toHaveLength(1); expect(remaining[0].id).toBe(ref2Id); }); }); describe('Duplicate Reference', () => { let refId: string; beforeEach(() => { const { addReference } = useBibliographyStore.getState(); refId = addReference(createMockReference('ref-1', { title: 'Original Title' })); }); it('should duplicate a reference', () => { const { duplicateReference, getReferences } = useBibliographyStore.getState(); const newId = duplicateReference(refId); expect(newId).toBeTruthy(); expect(newId).not.toBe(refId); expect(getReferences()).toHaveLength(2); }); it('should append (Copy) to title', () => { const { duplicateReference, getReferenceById } = useBibliographyStore.getState(); const newId = duplicateReference(refId); const duplicate = getReferenceById(newId); expect(duplicate?.title).toBe('Original Title (Copy)'); }); it('should generate unique ID for duplicate', () => { const { duplicateReference } = useBibliographyStore.getState(); const newId = duplicateReference(refId); expect(newId).toMatch(/^ref-\d+-[a-z0-9]+$/); }); it('should return empty string for non-existent reference', () => { const { duplicateReference } = useBibliographyStore.getState(); const result = duplicateReference('non-existent'); expect(result).toBe(''); }); it('should create metadata for duplicate', () => { const { duplicateReference } = useBibliographyStore.getState(); const newId = duplicateReference(refId); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata[newId]).toBeDefined(); expect(appMetadata[newId].id).toBe(newId); }); }); describe('Get Operations', () => { beforeEach(() => { const { addReference } = useBibliographyStore.getState(); addReference(createMockReference('ref-1')); addReference(createMockReference('ref-2')); }); it('should get all references with merged metadata', () => { const { getReferences } = useBibliographyStore.getState(); const refs = getReferences(); expect(refs).toHaveLength(2); expect(refs[0]._app).toBeDefined(); expect(refs[0]._app?.id).toBe('ref-1'); }); it('should get reference by ID', () => { const { getReferenceById } = useBibliographyStore.getState(); const ref = getReferenceById('ref-1'); expect(ref).toBeDefined(); expect(ref?.id).toBe('ref-1'); expect(ref?.title).toBe('Test Article ref-1'); }); it('should return undefined for non-existent ID', () => { const { getReferenceById } = useBibliographyStore.getState(); const ref = getReferenceById('non-existent'); expect(ref).toBeUndefined(); }); it('should get raw CSL data without metadata', () => { const { getCSLData } = useBibliographyStore.getState(); const data = getCSLData(); expect(data).toHaveLength(2); expect(data[0]._app).toBeUndefined(); }); }); describe('Set References', () => { it('should replace all references', () => { const { addReference, setReferences, getReferences } = useBibliographyStore.getState(); addReference(createMockReference('ref-1')); const newRefs = [ createMockReference('ref-2'), createMockReference('ref-3'), ]; setReferences(newRefs); const refs = getReferences(); expect(refs).toHaveLength(2); expect(refs.find(r => r.id === 'ref-1')).toBeUndefined(); }); it('should initialize metadata for all references', () => { const { setReferences } = useBibliographyStore.getState(); const newRefs = [ createMockReference('ref-1'), createMockReference('ref-2'), ]; setReferences(newRefs); const { appMetadata } = useBibliographyStore.getState(); expect(Object.keys(appMetadata)).toHaveLength(2); expect(appMetadata['ref-1']).toBeDefined(); expect(appMetadata['ref-2']).toBeDefined(); }); it('should clear old metadata', () => { const { addReference, setReferences } = useBibliographyStore.getState(); addReference(createMockReference('ref-old')); setReferences([createMockReference('ref-new')]); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata['ref-old']).toBeUndefined(); expect(appMetadata['ref-new']).toBeDefined(); }); }); describe('Import References', () => { beforeEach(() => { const { addReference } = useBibliographyStore.getState(); addReference(createMockReference('ref-1')); }); it('should append references to existing ones', () => { const { importReferences, getReferences } = useBibliographyStore.getState(); const newRefs = [ createMockReference('ref-2'), createMockReference('ref-3'), ]; importReferences(newRefs); expect(getReferences()).toHaveLength(3); }); it('should add metadata for new references', () => { const { importReferences } = useBibliographyStore.getState(); importReferences([createMockReference('ref-2')]); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata['ref-2']).toBeDefined(); }); it('should not overwrite existing metadata', () => { const { importReferences, updateMetadata } = useBibliographyStore.getState(); updateMetadata('ref-1', { tags: ['important'] }); importReferences([createMockReference('ref-1')]); // Duplicate ID const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata['ref-1'].tags).toEqual(['important']); }); }); describe('Clear All', () => { beforeEach(() => { const { addReference } = useBibliographyStore.getState(); addReference(createMockReference('ref-1')); addReference(createMockReference('ref-2')); }); it('should clear all references', () => { const { clearAll, getReferences } = useBibliographyStore.getState(); clearAll(); expect(getReferences()).toHaveLength(0); }); it('should clear all metadata', () => { const { clearAll } = useBibliographyStore.getState(); clearAll(); const { appMetadata } = useBibliographyStore.getState(); expect(Object.keys(appMetadata)).toHaveLength(0); }); }); describe('Update Metadata', () => { let refId: string; beforeEach(() => { const { addReference } = useBibliographyStore.getState(); refId = addReference(createMockReference('ref-1')); }); it('should update metadata tags', () => { const { updateMetadata } = useBibliographyStore.getState(); updateMetadata(refId, { tags: ['important', 'research'] }); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata[refId].tags).toEqual(['important', 'research']); }); it('should update metadata favorite', () => { const { updateMetadata } = useBibliographyStore.getState(); updateMetadata(refId, { favorite: true }); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata[refId].favorite).toBe(true); }); it('should update metadata color', () => { const { updateMetadata } = useBibliographyStore.getState(); updateMetadata(refId, { color: '#ff0000' }); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata[refId].color).toBe('#ff0000'); }); it('should update updatedAt timestamp', async () => { const { updateMetadata, appMetadata } = useBibliographyStore.getState(); const originalTime = appMetadata[refId].updatedAt; await new Promise(resolve => setTimeout(resolve, 10)); updateMetadata(refId, { tags: ['new'] }); const newTime = useBibliographyStore.getState().appMetadata[refId].updatedAt; expect(newTime).not.toBe(originalTime); }); it('should merge with existing metadata', () => { const { updateMetadata } = useBibliographyStore.getState(); updateMetadata(refId, { tags: ['tag1'] }); updateMetadata(refId, { favorite: true }); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata[refId].tags).toEqual(['tag1']); expect(appMetadata[refId].favorite).toBe(true); }); }); describe('Settings', () => { it('should update settings', () => { const { setSettings } = useBibliographyStore.getState(); setSettings({ defaultStyle: 'chicago', sortOrder: 'year', }); const newSettings = useBibliographyStore.getState().settings; expect(newSettings.defaultStyle).toBe('chicago'); expect(newSettings.sortOrder).toBe('year'); }); }); // Note: Format Reference, Format Bibliography, Parse Input, and Export As tests removed // These test citation.js library functionality, not our store logic describe('Clear Bibliography For Document Switch', () => { beforeEach(() => { const { addReference } = useBibliographyStore.getState(); addReference(createMockReference('ref-1')); }); it('should clear all references', () => { const { getReferences } = useBibliographyStore.getState(); clearBibliographyForDocumentSwitch(); expect(getReferences()).toHaveLength(0); }); it('should clear all metadata', () => { clearBibliographyForDocumentSwitch(); const newMetadata = useBibliographyStore.getState().appMetadata; expect(Object.keys(newMetadata)).toHaveLength(0); }); it('should reset settings to defaults', () => { const { setSettings } = useBibliographyStore.getState(); setSettings({ defaultStyle: 'chicago', sortOrder: 'year' }); clearBibliographyForDocumentSwitch(); const { settings } = useBibliographyStore.getState(); expect(settings.defaultStyle).toBe('apa'); expect(settings.sortOrder).toBe('author'); }); it('should create new Cite instance', () => { clearBibliographyForDocumentSwitch(); const { citeInstance } = useBibliographyStore.getState(); expect(citeInstance).toBeDefined(); }); }); describe('Edge Cases', () => { it('should handle operations with empty bibliography', () => { const { getReferences, getReferenceById } = useBibliographyStore.getState(); expect(getReferences()).toHaveLength(0); expect(getReferenceById('any')).toBeUndefined(); }); it('should handle rapid reference additions', () => { const { addReference, getReferences } = useBibliographyStore.getState(); const ids = Array.from({ length: 10 }, (_, i) => addReference(createMockReference(`ref-${i}`)) ); expect(getReferences()).toHaveLength(10); expect(new Set(ids).size).toBe(10); // All unique }); it('should handle updating non-existent reference gracefully', () => { const { updateReference, getReferences } = useBibliographyStore.getState(); // Should not throw expect(() => updateReference('non-existent', { title: 'Updated' })).not.toThrow(); expect(getReferences()).toHaveLength(0); }); it('should handle deleting non-existent reference gracefully', () => { const { deleteReference, getReferences } = useBibliographyStore.getState(); // Should not throw expect(() => deleteReference('non-existent')).not.toThrow(); expect(getReferences()).toHaveLength(0); }); it('should handle metadata operations on non-existent reference', () => { const { updateMetadata } = useBibliographyStore.getState(); updateMetadata('non-existent', { tags: ['test'] }); const { appMetadata } = useBibliographyStore.getState(); expect(appMetadata['non-existent']).toBeDefined(); }); it('should maintain data integrity across operations', () => { const { addReference, updateReference, duplicateReference, getReferences } = useBibliographyStore.getState(); const id1 = addReference(createMockReference('ref-1')); updateReference(id1, { title: 'Updated' }); duplicateReference(id1); const refs = getReferences(); expect(refs).toHaveLength(2); expect(refs[0].title).toBe('Updated'); expect(refs[1].title).toBe('Updated (Copy)'); }); }); });