Files
lemma/app/src/hooks/useGitOperations.test.ts

413 lines
12 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useGitOperations } from './useGitOperations';
import * as gitApi from '@/api/git';
// Mock dependencies
vi.mock('@/api/git');
vi.mock('@mantine/notifications', () => ({
notifications: {
show: vi.fn(),
},
}));
// Mock the workspace context
const mockWorkspaceData: {
currentWorkspace: { id: number; name: string; gitEnabled: boolean } | null;
} = {
currentWorkspace: {
id: 1,
name: 'test-workspace',
gitEnabled: true,
},
};
vi.mock('../contexts/WorkspaceDataContext', () => ({
useWorkspaceData: () => mockWorkspaceData,
}));
// Import notifications for assertions
import { notifications } from '@mantine/notifications';
describe('useGitOperations', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset workspace data to defaults
mockWorkspaceData.currentWorkspace = {
id: 1,
name: 'test-workspace',
gitEnabled: true,
};
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('handlePull', () => {
it('pulls changes successfully and shows success notification', async () => {
const mockPullChanges = vi.mocked(gitApi.pullChanges);
mockPullChanges.mockResolvedValue('Successfully pulled latest changes');
const { result } = renderHook(() => useGitOperations());
let pullResult: boolean | undefined;
await act(async () => {
pullResult = await result.current.handlePull();
});
expect(pullResult).toBe(true);
expect(mockPullChanges).toHaveBeenCalledWith('test-workspace');
expect(notifications.show).toHaveBeenCalledWith({
title: 'Success',
message: 'Successfully pulled latest changes',
color: 'green',
});
});
it('handles pull errors and shows error notification', async () => {
const mockPullChanges = vi.mocked(gitApi.pullChanges);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockPullChanges.mockRejectedValue(new Error('Pull failed'));
const { result } = renderHook(() => useGitOperations());
let pullResult: boolean | undefined;
await act(async () => {
pullResult = await result.current.handlePull();
});
expect(pullResult).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to pull latest changes:',
expect.any(Error)
);
expect(notifications.show).toHaveBeenCalledWith({
title: 'Error',
message: 'Failed to pull latest changes',
color: 'red',
});
consoleSpy.mockRestore();
});
it('returns false when no workspace is available', async () => {
mockWorkspaceData.currentWorkspace = null;
const { result } = renderHook(() => useGitOperations());
let pullResult: boolean | undefined;
await act(async () => {
pullResult = await result.current.handlePull();
});
expect(pullResult).toBe(false);
expect(gitApi.pullChanges).not.toHaveBeenCalled();
});
it('returns false when git is disabled', async () => {
mockWorkspaceData.currentWorkspace!.gitEnabled = false;
const { result } = renderHook(() => useGitOperations());
let pullResult: boolean | undefined;
await act(async () => {
pullResult = await result.current.handlePull();
});
expect(pullResult).toBe(false);
expect(gitApi.pullChanges).not.toHaveBeenCalled();
});
it('handles pull with different response messages', async () => {
const mockPullChanges = vi.mocked(gitApi.pullChanges);
mockPullChanges.mockResolvedValue('Already up to date');
const { result } = renderHook(() => useGitOperations());
await act(async () => {
await result.current.handlePull();
});
expect(notifications.show).toHaveBeenCalledWith({
title: 'Success',
message: 'Already up to date',
color: 'green',
});
});
});
describe('handleCommitAndPush', () => {
it('commits and pushes successfully with commit hash', async () => {
const mockCommitAndPush = vi.mocked(gitApi.commitAndPush);
mockCommitAndPush.mockResolvedValue('abc123def456');
const { result } = renderHook(() => useGitOperations());
await act(async () => {
await result.current.handleCommitAndPush('Add new feature');
});
expect(mockCommitAndPush).toHaveBeenCalledWith(
'test-workspace',
'Add new feature'
);
expect(notifications.show).toHaveBeenCalledWith({
title: 'Success',
message: 'Successfully committed and pushed changes abc123def456',
color: 'green',
});
});
it('handles commit errors and shows error notification', async () => {
const mockCommitAndPush = vi.mocked(gitApi.commitAndPush);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockCommitAndPush.mockRejectedValue(new Error('Commit failed'));
const { result } = renderHook(() => useGitOperations());
await act(async () => {
await result.current.handleCommitAndPush('Failed commit');
});
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to commit and push changes:',
expect.any(Error)
);
expect(notifications.show).toHaveBeenCalledWith({
title: 'Error',
message: 'Failed to commit and push changes',
color: 'red',
});
consoleSpy.mockRestore();
});
it('does nothing when no workspace is available', async () => {
mockWorkspaceData.currentWorkspace = null;
const { result } = renderHook(() => useGitOperations());
await act(async () => {
await result.current.handleCommitAndPush('Test commit');
});
expect(gitApi.commitAndPush).not.toHaveBeenCalled();
expect(notifications.show).not.toHaveBeenCalled();
});
it('does nothing when git is disabled', async () => {
mockWorkspaceData.currentWorkspace!.gitEnabled = false;
const { result } = renderHook(() => useGitOperations());
await act(async () => {
await result.current.handleCommitAndPush('Test commit');
});
expect(gitApi.commitAndPush).not.toHaveBeenCalled();
expect(notifications.show).not.toHaveBeenCalled();
});
it('handles empty commit messages', async () => {
const mockCommitAndPush = vi.mocked(gitApi.commitAndPush);
mockCommitAndPush.mockResolvedValue('xyz789abc123');
const { result } = renderHook(() => useGitOperations());
await act(async () => {
await result.current.handleCommitAndPush('');
});
expect(mockCommitAndPush).toHaveBeenCalledWith('test-workspace', '');
expect(notifications.show).toHaveBeenCalledWith({
title: 'Success',
message: 'Successfully committed and pushed changes xyz789abc123',
color: 'green',
});
});
it('handles long commit messages', async () => {
const mockCommitAndPush = vi.mocked(gitApi.commitAndPush);
mockCommitAndPush.mockResolvedValue('longcommithash123456789');
const longMessage =
'This is a very long commit message that describes in detail all the changes that were made to the codebase including bug fixes, new features, and documentation updates';
const { result } = renderHook(() => useGitOperations());
await act(async () => {
await result.current.handleCommitAndPush(longMessage);
});
expect(mockCommitAndPush).toHaveBeenCalledWith(
'test-workspace',
longMessage
);
expect(notifications.show).toHaveBeenCalledWith({
title: 'Success',
message:
'Successfully committed and pushed changes longcommithash123456789',
color: 'green',
});
});
it('handles commit with special characters in message', async () => {
const mockCommitAndPush = vi.mocked(gitApi.commitAndPush);
mockCommitAndPush.mockResolvedValue('special123hash');
const specialMessage =
'Fix: update file with special chars àáâãäå & symbols!@#$%';
const { result } = renderHook(() => useGitOperations());
await act(async () => {
await result.current.handleCommitAndPush(specialMessage);
});
expect(mockCommitAndPush).toHaveBeenCalledWith(
'test-workspace',
specialMessage
);
expect(notifications.show).toHaveBeenCalledWith({
title: 'Success',
message: 'Successfully committed and pushed changes special123hash',
color: 'green',
});
});
});
describe('workspace and settings dependencies', () => {
it('handles workspace changes correctly', async () => {
const mockPullChanges = vi.mocked(gitApi.pullChanges);
mockPullChanges.mockResolvedValue('Success');
const { result, rerender } = renderHook(() => useGitOperations());
// Test with initial workspace
await act(async () => {
await result.current.handlePull();
});
expect(mockPullChanges).toHaveBeenCalledWith('test-workspace');
// Change workspace
mockWorkspaceData.currentWorkspace = {
id: 2,
name: 'different-workspace',
gitEnabled: true,
};
rerender();
await act(async () => {
await result.current.handlePull();
});
expect(mockPullChanges).toHaveBeenCalledWith('different-workspace');
});
it('handles git settings changes correctly', async () => {
const { result, rerender } = renderHook(() => useGitOperations());
// Initially git is enabled
expect(mockWorkspaceData.currentWorkspace!.gitEnabled).toBe(true);
// Disable git
mockWorkspaceData.currentWorkspace!.gitEnabled = false;
rerender();
let pullResult: boolean | undefined;
await act(async () => {
pullResult = await result.current.handlePull();
});
expect(pullResult).toBe(false);
expect(gitApi.pullChanges).not.toHaveBeenCalled();
});
});
describe('hook interface', () => {
it('returns correct function interface', () => {
const { result } = renderHook(() => useGitOperations());
expect(typeof result.current.handlePull).toBe('function');
expect(typeof result.current.handleCommitAndPush).toBe('function');
});
it('functions are stable across re-renders', () => {
const { result, rerender } = renderHook(() => useGitOperations());
const initialHandlers = {
handlePull: result.current.handlePull,
handleCommitAndPush: result.current.handleCommitAndPush,
};
rerender();
expect(result.current.handlePull).toBe(initialHandlers.handlePull);
expect(result.current.handleCommitAndPush).toBe(
initialHandlers.handleCommitAndPush
);
});
});
describe('edge cases', () => {
it('handles null workspace gracefully', async () => {
mockWorkspaceData.currentWorkspace = null;
const { result } = renderHook(() => useGitOperations());
let pullResult: boolean | undefined;
await act(async () => {
pullResult = await result.current.handlePull();
});
expect(pullResult).toBe(false);
expect(gitApi.pullChanges).not.toHaveBeenCalled();
});
it('handles undefined workspace name gracefully', async () => {
mockWorkspaceData.currentWorkspace = {
id: 1,
name: undefined!,
gitEnabled: true,
};
const { result } = renderHook(() => useGitOperations());
let pullResult: boolean | undefined;
await act(async () => {
pullResult = await result.current.handlePull();
});
expect(pullResult).toBe(false);
expect(gitApi.pullChanges).not.toHaveBeenCalled();
});
it('handles missing settings gracefully', async () => {
mockWorkspaceData.currentWorkspace = {
id: 1,
name: 'test-workspace',
gitEnabled: undefined!,
};
const { result } = renderHook(() => useGitOperations());
let pullResult: boolean | undefined;
await act(async () => {
pullResult = await result.current.handlePull();
});
expect(pullResult).toBe(false);
expect(gitApi.pullChanges).not.toHaveBeenCalled();
});
});
});