From 942ff17c4f415856ce27a26555f275912538cb68 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 26 May 2025 21:53:52 +0200 Subject: [PATCH] Add tests for useFileContent and useFileOperations hooks --- app/src/hooks/useFileContent.test.ts | 386 +++++++++++++++++ app/src/hooks/useFileOperations.test.ts | 537 ++++++++++++++++++++++++ app/src/hooks/useGitOperations.ts | 4 +- 3 files changed, 925 insertions(+), 2 deletions(-) create mode 100644 app/src/hooks/useFileContent.test.ts create mode 100644 app/src/hooks/useFileOperations.test.ts diff --git a/app/src/hooks/useFileContent.test.ts b/app/src/hooks/useFileContent.test.ts new file mode 100644 index 0000000..558cc3a --- /dev/null +++ b/app/src/hooks/useFileContent.test.ts @@ -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); + }); + }); +}); diff --git a/app/src/hooks/useFileOperations.test.ts b/app/src/hooks/useFileOperations.test.ts new file mode 100644 index 0000000..20b914d --- /dev/null +++ b/app/src/hooks/useFileOperations.test.ts @@ -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); + }); + }); +}); diff --git a/app/src/hooks/useGitOperations.ts b/app/src/hooks/useGitOperations.ts index 601dff1..4e6d06f 100644 --- a/app/src/hooks/useGitOperations.ts +++ b/app/src/hooks/useGitOperations.ts @@ -16,10 +16,10 @@ export const useGitOperations = (): UseGitOperationsResult => { if (!currentWorkspace || !settings.gitEnabled) return false; try { - await pullChanges(currentWorkspace.name); + const message = await pullChanges(currentWorkspace.name); notifications.show({ title: 'Success', - message: 'Successfully pulled latest changes', + message: message || 'Successfully pulled latest changes', color: 'green', }); return true;