first commit of v.02 application

This commit is contained in:
KS Jannette
2026-05-07 23:20:30 -04:00
commit a6b0a95dfc
98 changed files with 21028 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
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

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