Syntax changes in front and back end dirs. These do not affect functionality and are purely intended to better capture and describe the app's functionality and purpose

This commit is contained in:
KS Jannette
2026-08-03 00:07:58 -04:00
parent 4a5e5d6612
commit 0352bdf516
42 changed files with 812 additions and 812 deletions

View File

@@ -1,50 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import * as notebooksApi from '../api/notebooks.js';
export function useNotebook() {
const [notebooks, setNotebooks] = useState([]);
const [activeNotebook, setActiveNotebook] = useState(null);
useEffect(() => {
notebooksApi.listNotebooks().then(setNotebooks).catch(console.error);
}, []);
const selectNotebook = useCallback(
(id) => {
const nb = notebooks.find((n) => n.id === id) || null;
setActiveNotebook(nb);
},
[notebooks],
);
const createNotebook = useCallback(async (name) => {
if (!name) return;
const nb = await notebooksApi.createNotebook(name);
setNotebooks((prev) => [...prev, nb]);
setActiveNotebook(nb);
}, []);
const deleteNotebook = useCallback(
async (id) => {
try {
await notebooksApi.deleteNotebook(id);
} catch (err) {
console.error('Failed to delete notebook', err);
return;
}
setNotebooks((prev) => prev.filter((n) => n.id !== id));
if (activeNotebook?.id === id) {
setActiveNotebook(null);
}
},
[activeNotebook],
);
return {
notebooks,
activeNotebook,
selectNotebook,
createNotebook,
deleteNotebook,
};
}

View File

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

View File

@@ -0,0 +1,50 @@
import { useState, useEffect, useCallback } from 'react';
import * as sourceCorpusApi from '../api/sourceCorpus.js';
export function useSourceCorpus() {
const [sourceCorpora, setSourceCorpora] = useState([]);
const [activeSourceCorpus, setActiveSourceCorpus] = useState(null);
useEffect(() => {
sourceCorpusApi.listSourceCorpora().then(setSourceCorpora).catch(console.error);
}, []);
const selectSourceCorpus = useCallback(
(id) => {
const corpus = sourceCorpora.find((n) => n.id === id) || null;
setActiveSourceCorpus(corpus);
},
[sourceCorpora],
);
const createSourceCorpus = useCallback(async (name) => {
if (!name) return;
const corpus = await sourceCorpusApi.createSourceCorpus(name);
setSourceCorpora((prev) => [...prev, corpus]);
setActiveSourceCorpus(corpus);
}, []);
const deleteSourceCorpus = useCallback(
async (id) => {
try {
await sourceCorpusApi.deleteSourceCorpus(id);
} catch (err) {
console.error('Failed to delete sourceCorpus', err);
return;
}
setSourceCorpora((prev) => prev.filter((n) => n.id !== id));
if (activeSourceCorpus?.id === id) {
setActiveSourceCorpus(null);
}
},
[activeSourceCorpus],
);
return {
sourceCorpora,
activeSourceCorpus,
selectSourceCorpus,
createSourceCorpus,
deleteSourceCorpus,
};
}

View File

@@ -0,0 +1,176 @@
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();
});
});