mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Add tests for useFileContent and useFileOperations hooks
This commit is contained in:
386
app/src/hooks/useFileContent.test.ts
Normal file
386
app/src/hooks/useFileContent.test.ts
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
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 manual setting of unsaved changes state', () => {
|
||||||
|
const { result } = renderHook(() => useFileContent(null));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setHasUnsavedChanges(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.hasUnsavedChanges).toBe(true);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setHasUnsavedChanges(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
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('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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
537
app/src/hooks/useFileOperations.test.ts
Normal file
537
app/src/hooks/useFileOperations.test.ts
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { useFileOperations } from './useFileOperations';
|
||||||
|
import * as fileApi from '@/api/file';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('@/api/file');
|
||||||
|
vi.mock('@mantine/notifications', () => ({
|
||||||
|
notifications: {
|
||||||
|
show: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the workspace context and git operations
|
||||||
|
const mockWorkspaceData: {
|
||||||
|
currentWorkspace: { id: number; name: string } | null;
|
||||||
|
settings: {
|
||||||
|
gitAutoCommit: boolean;
|
||||||
|
gitEnabled: boolean;
|
||||||
|
gitCommitMsgTemplate: string;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
currentWorkspace: {
|
||||||
|
id: 1,
|
||||||
|
name: 'test-workspace',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
gitAutoCommit: false,
|
||||||
|
gitEnabled: false,
|
||||||
|
gitCommitMsgTemplate: '${action} ${filename}',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGitOperations = {
|
||||||
|
handleCommitAndPush: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('../contexts/WorkspaceDataContext', () => ({
|
||||||
|
useWorkspaceData: () => mockWorkspaceData,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./useGitOperations', () => ({
|
||||||
|
useGitOperations: () => mockGitOperations,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import notifications for assertions
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
|
||||||
|
describe('useFileOperations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
// Reset workspace data to defaults
|
||||||
|
mockWorkspaceData.currentWorkspace = {
|
||||||
|
id: 1,
|
||||||
|
name: 'test-workspace',
|
||||||
|
};
|
||||||
|
mockWorkspaceData.settings = {
|
||||||
|
gitAutoCommit: false,
|
||||||
|
gitEnabled: false,
|
||||||
|
gitCommitMsgTemplate: '${action} ${filename}',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleSave', () => {
|
||||||
|
it('saves file successfully and shows success notification', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'test.md',
|
||||||
|
size: 1024,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let saveResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
saveResult = await result.current.handleSave(
|
||||||
|
'test.md',
|
||||||
|
'# Test Content'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(saveResult).toBe(true);
|
||||||
|
expect(mockSaveFile).toHaveBeenCalledWith(
|
||||||
|
'test-workspace',
|
||||||
|
'test.md',
|
||||||
|
'# Test Content'
|
||||||
|
);
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Success',
|
||||||
|
message: 'File saved successfully',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles save errors and shows error notification', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
const consoleSpy = vi
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
mockSaveFile.mockRejectedValue(new Error('Save failed'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let saveResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
saveResult = await result.current.handleSave(
|
||||||
|
'test.md',
|
||||||
|
'# Test Content'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(saveResult).toBe(false);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
'Error saving file:',
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to save file',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when no workspace is available', async () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = null;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let saveResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
saveResult = await result.current.handleSave(
|
||||||
|
'test.md',
|
||||||
|
'# Test Content'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(saveResult).toBe(false);
|
||||||
|
expect(fileApi.saveFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers auto-commit when enabled', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'test.md',
|
||||||
|
size: 1024,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable auto-commit
|
||||||
|
mockWorkspaceData.settings.gitAutoCommit = true;
|
||||||
|
mockWorkspaceData.settings.gitEnabled = true;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleSave('test.md', '# Test Content');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).toHaveBeenCalledWith(
|
||||||
|
'Update test.md'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses custom commit message template', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'docs/readme.md',
|
||||||
|
size: 1024,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable auto-commit with custom template
|
||||||
|
mockWorkspaceData.settings.gitAutoCommit = true;
|
||||||
|
mockWorkspaceData.settings.gitEnabled = true;
|
||||||
|
mockWorkspaceData.settings.gitCommitMsgTemplate =
|
||||||
|
'Modified ${filename} - ${action}';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleSave('docs/readme.md', '# Documentation');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).toHaveBeenCalledWith(
|
||||||
|
'Modified docs/readme.md - update'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleDelete', () => {
|
||||||
|
it('deletes file successfully and shows success notification', async () => {
|
||||||
|
const mockDeleteFile = vi.mocked(fileApi.deleteFile);
|
||||||
|
mockDeleteFile.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let deleteResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
deleteResult = await result.current.handleDelete('test.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteResult).toBe(true);
|
||||||
|
expect(mockDeleteFile).toHaveBeenCalledWith('test-workspace', 'test.md');
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Success',
|
||||||
|
message: 'File deleted successfully',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles delete errors and shows error notification', async () => {
|
||||||
|
const mockDeleteFile = vi.mocked(fileApi.deleteFile);
|
||||||
|
const consoleSpy = vi
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
mockDeleteFile.mockRejectedValue(new Error('Delete failed'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let deleteResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
deleteResult = await result.current.handleDelete('test.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteResult).toBe(false);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
'Error deleting file:',
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to delete file',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when no workspace is available', async () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = null;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let deleteResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
deleteResult = await result.current.handleDelete('test.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteResult).toBe(false);
|
||||||
|
expect(fileApi.deleteFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers auto-commit when enabled', async () => {
|
||||||
|
const mockDeleteFile = vi.mocked(fileApi.deleteFile);
|
||||||
|
mockDeleteFile.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
// Enable auto-commit
|
||||||
|
mockWorkspaceData.settings.gitAutoCommit = true;
|
||||||
|
mockWorkspaceData.settings.gitEnabled = true;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleDelete('old-file.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).toHaveBeenCalledWith(
|
||||||
|
'Delete old-file.md'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleCreate', () => {
|
||||||
|
it('creates file successfully with default content', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'new.md',
|
||||||
|
size: 0,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let createResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
createResult = await result.current.handleCreate('new.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResult).toBe(true);
|
||||||
|
expect(mockSaveFile).toHaveBeenCalledWith('test-workspace', 'new.md', '');
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Success',
|
||||||
|
message: 'File created successfully',
|
||||||
|
color: 'green',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates file with custom initial content', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'template.md',
|
||||||
|
size: 1024,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let createResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
createResult = await result.current.handleCreate(
|
||||||
|
'template.md',
|
||||||
|
'# Template\n\nContent here'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResult).toBe(true);
|
||||||
|
expect(mockSaveFile).toHaveBeenCalledWith(
|
||||||
|
'test-workspace',
|
||||||
|
'template.md',
|
||||||
|
'# Template\n\nContent here'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles create errors and shows error notification', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
const consoleSpy = vi
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
mockSaveFile.mockRejectedValue(new Error('Create failed'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let createResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
createResult = await result.current.handleCreate('new.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResult).toBe(false);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
'Error creating new file:',
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to create new file',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when no workspace is available', async () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = null;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
let createResult: boolean | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
createResult = await result.current.handleCreate('new.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResult).toBe(false);
|
||||||
|
expect(fileApi.saveFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers auto-commit when enabled', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'new-file.md',
|
||||||
|
size: 1024,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable auto-commit
|
||||||
|
mockWorkspaceData.settings.gitAutoCommit = true;
|
||||||
|
mockWorkspaceData.settings.gitEnabled = true;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleCreate('new-file.md', 'Initial content');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).toHaveBeenCalledWith(
|
||||||
|
'Create new-file.md'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('auto-commit behavior', () => {
|
||||||
|
it('does not auto-commit when git is disabled', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'test.md',
|
||||||
|
size: 1024,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable auto-commit but disable git
|
||||||
|
mockWorkspaceData.settings.gitAutoCommit = true;
|
||||||
|
mockWorkspaceData.settings.gitEnabled = false;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleSave('test.md', 'content');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not auto-commit when auto-commit is disabled', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'test.md',
|
||||||
|
size: 1024,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable git but disable auto-commit
|
||||||
|
mockWorkspaceData.settings.gitAutoCommit = false;
|
||||||
|
mockWorkspaceData.settings.gitEnabled = true;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleSave('test.md', 'content');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('capitalizes commit messages correctly', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'test.md',
|
||||||
|
size: 1024,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable auto-commit with lowercase template
|
||||||
|
mockWorkspaceData.settings.gitAutoCommit = true;
|
||||||
|
mockWorkspaceData.settings.gitEnabled = true;
|
||||||
|
mockWorkspaceData.settings.gitCommitMsgTemplate = 'updated ${filename}';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleSave('test.md', 'content');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).toHaveBeenCalledWith(
|
||||||
|
'Updated test.md'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles different file actions correctly', async () => {
|
||||||
|
const mockSaveFile = vi.mocked(fileApi.saveFile);
|
||||||
|
const mockDeleteFile = vi.mocked(fileApi.deleteFile);
|
||||||
|
|
||||||
|
mockSaveFile.mockResolvedValue({
|
||||||
|
filePath: 'test.md',
|
||||||
|
size: 1024,
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
mockDeleteFile.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
// Enable auto-commit
|
||||||
|
mockWorkspaceData.settings.gitAutoCommit = true;
|
||||||
|
mockWorkspaceData.settings.gitEnabled = true;
|
||||||
|
mockWorkspaceData.settings.gitCommitMsgTemplate =
|
||||||
|
'${action}: ${filename}';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
// Test create action
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleCreate('new.md');
|
||||||
|
});
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).toHaveBeenCalledWith(
|
||||||
|
'Create: new.md'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test update action
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleSave('existing.md', 'content');
|
||||||
|
});
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).toHaveBeenCalledWith(
|
||||||
|
'Update: existing.md'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test delete action
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleDelete('old.md');
|
||||||
|
});
|
||||||
|
expect(mockGitOperations.handleCommitAndPush).toHaveBeenCalledWith(
|
||||||
|
'Delete: old.md'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hook interface', () => {
|
||||||
|
it('returns correct function interface', () => {
|
||||||
|
const { result } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
expect(typeof result.current.handleSave).toBe('function');
|
||||||
|
expect(typeof result.current.handleDelete).toBe('function');
|
||||||
|
expect(typeof result.current.handleCreate).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('functions are stable across re-renders', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useFileOperations());
|
||||||
|
|
||||||
|
const initialHandlers = {
|
||||||
|
handleSave: result.current.handleSave,
|
||||||
|
handleDelete: result.current.handleDelete,
|
||||||
|
handleCreate: result.current.handleCreate,
|
||||||
|
};
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.handleSave).toBe(initialHandlers.handleSave);
|
||||||
|
expect(result.current.handleDelete).toBe(initialHandlers.handleDelete);
|
||||||
|
expect(result.current.handleCreate).toBe(initialHandlers.handleCreate);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,10 +16,10 @@ export const useGitOperations = (): UseGitOperationsResult => {
|
|||||||
if (!currentWorkspace || !settings.gitEnabled) return false;
|
if (!currentWorkspace || !settings.gitEnabled) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pullChanges(currentWorkspace.name);
|
const message = await pullChanges(currentWorkspace.name);
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'Successfully pulled latest changes',
|
message: message || 'Successfully pulled latest changes',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user