mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
510 lines
16 KiB
TypeScript
510 lines
16 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
import { useFileContent } from './useFileContent';
|
|
import * as fileApi from '@/api/file';
|
|
import * as fileHelpers from '@/utils/fileHelpers';
|
|
import { DEFAULT_FILE } from '@/types/models';
|
|
|
|
// Mock dependencies
|
|
vi.mock('@/api/file');
|
|
vi.mock('@/utils/fileHelpers');
|
|
|
|
// Create a mock workspace context hook
|
|
const mockWorkspaceData: {
|
|
currentWorkspace: { id: number; name: string } | null;
|
|
} = {
|
|
currentWorkspace: {
|
|
id: 1,
|
|
name: 'test-workspace',
|
|
},
|
|
};
|
|
|
|
vi.mock('../contexts/WorkspaceDataContext', () => ({
|
|
useWorkspaceData: () => mockWorkspaceData,
|
|
}));
|
|
|
|
describe('useFileContent', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Reset workspace data
|
|
mockWorkspaceData.currentWorkspace = {
|
|
id: 1,
|
|
name: 'test-workspace',
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('initial state', () => {
|
|
it('returns default content and no unsaved changes initially', () => {
|
|
const { result } = renderHook(() => useFileContent(null));
|
|
|
|
expect(result.current.content).toBe(DEFAULT_FILE.content);
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
});
|
|
|
|
it('provides setters for content and unsaved changes', () => {
|
|
const { result } = renderHook(() => useFileContent(null));
|
|
|
|
expect(typeof result.current.setContent).toBe('function');
|
|
expect(typeof result.current.setHasUnsavedChanges).toBe('function');
|
|
expect(typeof result.current.loadFileContent).toBe('function');
|
|
expect(typeof result.current.handleContentChange).toBe('function');
|
|
});
|
|
});
|
|
|
|
describe('loading file content', () => {
|
|
it('loads default file content when selectedFile is DEFAULT_FILE.path', async () => {
|
|
const { result } = renderHook(() => useFileContent(DEFAULT_FILE.path));
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe(DEFAULT_FILE.content);
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
});
|
|
|
|
expect(fileApi.getFileContent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('loads file content from API for regular files', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
|
|
mockGetFileContent.mockResolvedValue('# Test Content');
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result } = renderHook(() => useFileContent('test.md'));
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('# Test Content');
|
|
});
|
|
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
expect(mockGetFileContent).toHaveBeenCalledWith(
|
|
'test-workspace',
|
|
'test.md'
|
|
);
|
|
});
|
|
|
|
it('sets empty content for image files', async () => {
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
mockIsImageFile.mockReturnValue(true);
|
|
|
|
const { result } = renderHook(() => useFileContent('image.png'));
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('');
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
});
|
|
|
|
expect(fileApi.getFileContent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles API errors gracefully', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
const consoleSpy = vi
|
|
.spyOn(console, 'error')
|
|
.mockImplementation(() => {});
|
|
|
|
mockGetFileContent.mockRejectedValue(new Error('API Error'));
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result } = renderHook(() => useFileContent('error.md'));
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('');
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
});
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
'Error loading file content:',
|
|
expect.any(Error)
|
|
);
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('does not load content when no workspace is available', () => {
|
|
// Mock no workspace
|
|
mockWorkspaceData.currentWorkspace = null;
|
|
|
|
const { result } = renderHook(() => useFileContent('test.md'));
|
|
|
|
expect(result.current.content).toBe(DEFAULT_FILE.content);
|
|
expect(fileApi.getFileContent).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('content changes', () => {
|
|
it('updates content and tracks unsaved changes', () => {
|
|
const { result } = renderHook(() => useFileContent(null));
|
|
|
|
act(() => {
|
|
result.current.handleContentChange('New content');
|
|
});
|
|
|
|
expect(result.current.content).toBe('New content');
|
|
expect(result.current.hasUnsavedChanges).toBe(true);
|
|
});
|
|
|
|
it('does not mark as unsaved when content matches original', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
|
|
mockGetFileContent.mockResolvedValue('Original content');
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result } = renderHook(() => useFileContent('test.md'));
|
|
|
|
// Wait for initial load
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Original content');
|
|
});
|
|
|
|
// Change content
|
|
act(() => {
|
|
result.current.handleContentChange('Modified content');
|
|
});
|
|
|
|
expect(result.current.hasUnsavedChanges).toBe(true);
|
|
|
|
// Change back to original
|
|
act(() => {
|
|
result.current.handleContentChange('Original content');
|
|
});
|
|
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
});
|
|
|
|
it('allows direct content setting', () => {
|
|
const { result } = renderHook(() => useFileContent(null));
|
|
|
|
act(() => {
|
|
result.current.setContent('Direct content');
|
|
});
|
|
|
|
expect(result.current.content).toBe('Direct content');
|
|
// Note: setContent doesn't automatically update unsaved changes
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('file changes', () => {
|
|
it('reloads content when selectedFile changes', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
|
|
mockGetFileContent
|
|
.mockResolvedValueOnce('First file content')
|
|
.mockResolvedValueOnce('Second file content');
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result, rerender } = renderHook(
|
|
({ selectedFile }) => useFileContent(selectedFile),
|
|
{ initialProps: { selectedFile: 'first.md' } }
|
|
);
|
|
|
|
// Wait for first file to load
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('First file content');
|
|
});
|
|
|
|
// Change to second file
|
|
rerender({ selectedFile: 'second.md' });
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Second file content');
|
|
});
|
|
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
expect(mockGetFileContent).toHaveBeenCalledTimes(2);
|
|
expect(mockGetFileContent).toHaveBeenNthCalledWith(
|
|
1,
|
|
'test-workspace',
|
|
'first.md'
|
|
);
|
|
expect(mockGetFileContent).toHaveBeenNthCalledWith(
|
|
2,
|
|
'test-workspace',
|
|
'second.md'
|
|
);
|
|
});
|
|
|
|
it('resets unsaved changes when file changes', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
|
|
mockGetFileContent
|
|
.mockResolvedValueOnce('File content')
|
|
.mockResolvedValueOnce('Other file content');
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result, rerender } = renderHook(
|
|
({ selectedFile }) => useFileContent(selectedFile),
|
|
{ initialProps: { selectedFile: 'first.md' } }
|
|
);
|
|
|
|
// Wait for initial load and make changes
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('File content');
|
|
});
|
|
|
|
act(() => {
|
|
result.current.handleContentChange('Modified content');
|
|
});
|
|
|
|
expect(result.current.hasUnsavedChanges).toBe(true);
|
|
|
|
// Change file
|
|
rerender({ selectedFile: 'second.md' });
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
});
|
|
});
|
|
|
|
it('does not reload when selectedFile is null', () => {
|
|
const { result } = renderHook(() => useFileContent(null));
|
|
|
|
expect(result.current.content).toBe(DEFAULT_FILE.content);
|
|
expect(fileApi.getFileContent).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('manual loadFileContent', () => {
|
|
it('can manually load file content', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
|
|
mockGetFileContent.mockResolvedValue('Manually loaded content');
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result } = renderHook(() => useFileContent(null));
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileContent('manual.md');
|
|
});
|
|
|
|
expect(result.current.content).toBe('Manually loaded content');
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
expect(mockGetFileContent).toHaveBeenCalledWith(
|
|
'test-workspace',
|
|
'manual.md'
|
|
);
|
|
});
|
|
|
|
it('handles manual load errors', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
const consoleSpy = vi
|
|
.spyOn(console, 'error')
|
|
.mockImplementation(() => {});
|
|
|
|
mockGetFileContent.mockRejectedValue(new Error('Manual load error'));
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result } = renderHook(() => useFileContent(null));
|
|
|
|
await act(async () => {
|
|
await result.current.loadFileContent('error.md');
|
|
});
|
|
|
|
expect(result.current.content).toBe('');
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
'Error loading file content:',
|
|
expect.any(Error)
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('workspace dependency changes', () => {
|
|
it('reloads content when workspace changes while file is selected', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
|
|
mockGetFileContent
|
|
.mockResolvedValueOnce('Content from workspace 1')
|
|
.mockResolvedValueOnce('Content from workspace 2');
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result, rerender } = renderHook(() => useFileContent('test.md'));
|
|
|
|
// Wait for initial load from workspace 1
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Content from workspace 1');
|
|
});
|
|
|
|
// Change workspace
|
|
mockWorkspaceData.currentWorkspace = {
|
|
id: 2,
|
|
name: 'different-workspace',
|
|
};
|
|
|
|
rerender();
|
|
|
|
// Should reload content from new workspace
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Content from workspace 2');
|
|
});
|
|
|
|
expect(mockGetFileContent).toHaveBeenCalledWith(
|
|
'test-workspace',
|
|
'test.md'
|
|
);
|
|
expect(mockGetFileContent).toHaveBeenCalledWith(
|
|
'different-workspace',
|
|
'test.md'
|
|
);
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
});
|
|
|
|
it('clears content when workspace becomes null', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
|
|
mockGetFileContent.mockResolvedValue('Initial content');
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result, rerender } = renderHook(() => useFileContent('test.md'));
|
|
|
|
// Wait for initial load
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Initial content');
|
|
});
|
|
|
|
expect(mockGetFileContent).toHaveBeenCalledTimes(1);
|
|
vi.clearAllMocks(); // Clear previous calls
|
|
|
|
// Remove workspace
|
|
mockWorkspaceData.currentWorkspace = null;
|
|
rerender();
|
|
|
|
// Content should remain the same (no clearing happens when workspace becomes null)
|
|
// The hook keeps the current content and just prevents new loads
|
|
expect(result.current.content).toBe('Initial content');
|
|
expect(result.current.hasUnsavedChanges).toBe(false);
|
|
expect(mockGetFileContent).not.toHaveBeenCalled(); // No new API calls
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('handles empty string selectedFile', () => {
|
|
const { result } = renderHook(() => useFileContent(''));
|
|
|
|
// Empty string should not trigger file loading
|
|
expect(result.current.content).toBe(DEFAULT_FILE.content);
|
|
expect(fileApi.getFileContent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles rapid file changes', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
|
|
// Set up different responses for each file
|
|
mockGetFileContent
|
|
.mockImplementationOnce(() => Promise.resolve('Content 1'))
|
|
.mockImplementationOnce(() => Promise.resolve('Content 2'))
|
|
.mockImplementationOnce(() => Promise.resolve('Content 3'));
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result, rerender } = renderHook(
|
|
({ selectedFile }) => useFileContent(selectedFile),
|
|
{ initialProps: { selectedFile: 'file1.md' } }
|
|
);
|
|
|
|
// Wait for initial load
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Content 1');
|
|
});
|
|
|
|
// Rapidly change files
|
|
rerender({ selectedFile: 'file2.md' });
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Content 2');
|
|
});
|
|
|
|
rerender({ selectedFile: 'file3.md' });
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Content 3');
|
|
});
|
|
|
|
expect(mockGetFileContent).toHaveBeenCalledTimes(3);
|
|
});
|
|
});
|
|
|
|
describe('function stability', () => {
|
|
it('maintains stable function references across re-renders and workspace changes', async () => {
|
|
const mockGetFileContent = vi.mocked(fileApi.getFileContent);
|
|
const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
|
|
|
|
// Mock API calls for both workspaces
|
|
mockGetFileContent
|
|
.mockResolvedValueOnce('Content from workspace 1')
|
|
.mockResolvedValueOnce('Content from workspace 2');
|
|
mockIsImageFile.mockReturnValue(false);
|
|
|
|
const { result, rerender } = renderHook(() => useFileContent('test.md'));
|
|
|
|
// Wait for initial load to complete
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Content from workspace 1');
|
|
});
|
|
|
|
const initialFunctions = {
|
|
setContent: result.current.setContent,
|
|
setHasUnsavedChanges: result.current.setHasUnsavedChanges,
|
|
loadFileContent: result.current.loadFileContent,
|
|
handleContentChange: result.current.handleContentChange,
|
|
};
|
|
|
|
// Re-render with different file
|
|
rerender();
|
|
|
|
expect(result.current.setContent).toBe(initialFunctions.setContent);
|
|
expect(result.current.setHasUnsavedChanges).toBe(
|
|
initialFunctions.setHasUnsavedChanges
|
|
);
|
|
expect(result.current.loadFileContent).toBe(
|
|
initialFunctions.loadFileContent
|
|
);
|
|
expect(result.current.handleContentChange).toBe(
|
|
initialFunctions.handleContentChange
|
|
);
|
|
|
|
// Change workspace
|
|
act(() => {
|
|
mockWorkspaceData.currentWorkspace = {
|
|
id: 2,
|
|
name: 'different-workspace',
|
|
};
|
|
});
|
|
|
|
rerender();
|
|
|
|
// Wait for content to load from new workspace
|
|
await waitFor(() => {
|
|
expect(result.current.content).toBe('Content from workspace 2');
|
|
});
|
|
|
|
// Functions should still be stable (except handleContentChange which depends on originalContent)
|
|
expect(result.current.setContent).toBe(initialFunctions.setContent);
|
|
expect(result.current.setHasUnsavedChanges).toBe(
|
|
initialFunctions.setHasUnsavedChanges
|
|
);
|
|
expect(result.current.loadFileContent).not.toBe(
|
|
initialFunctions.loadFileContent
|
|
);
|
|
// handleContentChange depends on originalContent which changes when workspace changes
|
|
expect(result.current.handleContentChange).not.toBe(
|
|
initialFunctions.handleContentChange
|
|
);
|
|
});
|
|
});
|
|
});
|