import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderHook, act, waitFor } from '@testing-library/react'; vi.mock('../api/sourceCorpus.js', () => ({ listSourceCorpora: vi.fn(), createSourceCorpus: vi.fn(), deleteSourceCorpus: vi.fn(), })); import { useSourceCorpus } from './useSourceCorpus.js'; import * as sourceCorpusApi from '../api/sourceCorpus.js'; const sourceCorpora = [ { id: 'nb-1', name: 'Research' }, { id: 'nb-2', name: 'Personal' }, ]; beforeEach(() => { vi.resetAllMocks(); sourceCorpusApi.listSourceCorpora.mockResolvedValue(sourceCorpora); sourceCorpusApi.createSourceCorpus.mockImplementation(async (name) => ({ id: `nb-${Date.now()}`, name, })); sourceCorpusApi.deleteSourceCorpus.mockResolvedValue({}); }); describe('useSourceCorpus', () => { it('loads sourceCorpora on mount', async () => { const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => { expect(result.current.sourceCorpora).toEqual(sourceCorpora); }); expect(sourceCorpusApi.listSourceCorpora).toHaveBeenCalledOnce(); }); it('starts with no active sourceCorpus', () => { const { result } = renderHook(() => useSourceCorpus()); expect(result.current.activeSourceCorpus).toBeNull(); }); it('selectSourceCorpus sets the active sourceCorpus', async () => { const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => { expect(result.current.sourceCorpora).toHaveLength(2); }); act(() => { result.current.selectSourceCorpus('nb-2'); }); expect(result.current.activeSourceCorpus).toEqual({ id: 'nb-2', name: 'Personal' }); }); it('selectSourceCorpus sets null for unknown id', async () => { const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => { expect(result.current.sourceCorpora).toHaveLength(2); }); act(() => { result.current.selectSourceCorpus('nb-2'); }); expect(result.current.activeSourceCorpus).not.toBeNull(); act(() => { result.current.selectSourceCorpus('nonexistent'); }); expect(result.current.activeSourceCorpus).toBeNull(); }); it('createSourceCorpus calls API and adds to list', async () => { const newCorpus = { id: 'nb-new', name: 'New One' }; sourceCorpusApi.createSourceCorpus.mockResolvedValue(newCorpus); const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => expect(result.current.sourceCorpora).toHaveLength(2)); await act(async () => { await result.current.createSourceCorpus('New One'); }); expect(sourceCorpusApi.createSourceCorpus).toHaveBeenCalledWith('New One'); expect(result.current.sourceCorpora).toHaveLength(3); expect(result.current.sourceCorpora[2]).toEqual(newCorpus); expect(result.current.activeSourceCorpus).toEqual(newCorpus); }); it('createSourceCorpus does nothing for empty name', async () => { const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => expect(result.current.sourceCorpora).toHaveLength(2)); await act(async () => { await result.current.createSourceCorpus(''); }); expect(sourceCorpusApi.createSourceCorpus).not.toHaveBeenCalled(); expect(result.current.sourceCorpora).toHaveLength(2); }); it('deleteSourceCorpus removes from list', async () => { const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => expect(result.current.sourceCorpora).toHaveLength(2)); await act(async () => { await result.current.deleteSourceCorpus('nb-1'); }); expect(sourceCorpusApi.deleteSourceCorpus).toHaveBeenCalledWith('nb-1'); expect(result.current.sourceCorpora).toHaveLength(1); expect(result.current.sourceCorpora[0].id).toBe('nb-2'); }); it('deleteSourceCorpus clears activeSourceCorpus if it was the deleted one', async () => { const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => expect(result.current.sourceCorpora).toHaveLength(2)); act(() => { result.current.selectSourceCorpus('nb-1'); }); expect(result.current.activeSourceCorpus?.id).toBe('nb-1'); await act(async () => { await result.current.deleteSourceCorpus('nb-1'); }); expect(result.current.activeSourceCorpus).toBeNull(); }); it('deleteSourceCorpus preserves activeSourceCorpus if different one deleted', async () => { const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => expect(result.current.sourceCorpora).toHaveLength(2)); act(() => { result.current.selectSourceCorpus('nb-2'); }); await act(async () => { await result.current.deleteSourceCorpus('nb-1'); }); expect(result.current.activeSourceCorpus).toEqual({ id: 'nb-2', name: 'Personal' }); }); it('deleteSourceCorpus does not remove from list on API error', async () => { sourceCorpusApi.deleteSourceCorpus.mockRejectedValue(new Error('Server error')); vi.spyOn(console, 'error').mockImplementation(() => {}); const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => expect(result.current.sourceCorpora).toHaveLength(2)); await act(async () => { await result.current.deleteSourceCorpus('nb-1'); }); expect(result.current.sourceCorpora).toHaveLength(2); console.error.mockRestore(); }); it('handles listSourceCorpora API failure gracefully', async () => { sourceCorpusApi.listSourceCorpora.mockRejectedValue(new Error('Network error')); vi.spyOn(console, 'error').mockImplementation(() => {}); const { result } = renderHook(() => useSourceCorpus()); await waitFor(() => { expect(sourceCorpusApi.listSourceCorpora).toHaveBeenCalled(); }); expect(result.current.sourceCorpora).toEqual([]); console.error.mockRestore(); }); });