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

576 lines
18 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useWorkspaceOperations } from './useWorkspaceOperations';
import * as workspaceApi from '@/api/workspace';
import { Theme, type Workspace } from '@/types/models';
// Mock dependencies
vi.mock('@/api/workspace');
vi.mock('@mantine/notifications', () => ({
notifications: {
show: vi.fn(),
},
}));
// Mock workspace data context
const mockWorkspaceData: {
currentWorkspace: Workspace | null;
loadWorkspaceData: ReturnType<typeof vi.fn>;
loadWorkspaces: ReturnType<typeof vi.fn>;
setCurrentWorkspace: ReturnType<typeof vi.fn>;
} = {
currentWorkspace: {
id: 1,
userId: 1,
name: 'test-workspace',
createdAt: '2024-01-01T00:00:00Z',
theme: Theme.Light,
autoSave: false,
showHiddenFiles: false,
gitEnabled: false,
gitUrl: '',
gitUser: '',
gitToken: '',
gitAutoCommit: false,
gitCommitMsgTemplate: '${action} ${filename}',
gitCommitName: '',
gitCommitEmail: '',
},
loadWorkspaceData: vi.fn(),
loadWorkspaces: vi.fn(),
setCurrentWorkspace: vi.fn(),
};
// Mock theme context
const mockTheme = {
updateColorScheme: vi.fn(),
};
vi.mock('../contexts/WorkspaceDataContext', () => ({
useWorkspaceData: () => mockWorkspaceData,
}));
vi.mock('../contexts/ThemeContext', () => ({
useTheme: () => mockTheme,
}));
// Import notifications for assertions
import { notifications } from '@mantine/notifications';
// Mock workspaces for testing
const mockWorkspaces: Workspace[] = [
{
id: 1,
userId: 1,
name: 'workspace-1',
createdAt: '2024-01-01T00:00:00Z',
theme: Theme.Light,
autoSave: false,
showHiddenFiles: false,
gitEnabled: false,
gitUrl: '',
gitUser: '',
gitToken: '',
gitAutoCommit: false,
gitCommitMsgTemplate: '${action} ${filename}',
gitCommitName: '',
gitCommitEmail: '',
},
{
id: 2,
userId: 1,
name: 'workspace-2',
createdAt: '2024-01-02T00:00:00Z',
theme: Theme.Dark,
autoSave: true,
showHiddenFiles: true,
gitEnabled: true,
gitUrl: 'https://github.com/user/repo.git',
gitUser: 'user',
gitToken: 'token',
gitAutoCommit: true,
gitCommitMsgTemplate: 'auto: ${action} ${filename}',
gitCommitName: 'Test User',
gitCommitEmail: 'test@example.com',
},
];
describe('useWorkspaceOperations', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset workspace data to defaults
mockWorkspaceData.currentWorkspace = {
id: 1,
userId: 1,
name: 'test-workspace',
createdAt: '2024-01-01T00:00:00Z',
theme: Theme.Light,
autoSave: false,
showHiddenFiles: false,
gitEnabled: false,
gitUrl: '',
gitUser: '',
gitToken: '',
gitAutoCommit: false,
gitCommitMsgTemplate: '${action} ${filename}',
gitCommitName: '',
gitCommitEmail: '',
};
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('switchWorkspace', () => {
it('switches workspace successfully', async () => {
const mockUpdateLastWorkspaceName = vi.mocked(
workspaceApi.updateLastWorkspaceName
);
mockUpdateLastWorkspaceName.mockResolvedValue(undefined);
mockWorkspaceData.loadWorkspaceData.mockResolvedValue(undefined);
mockWorkspaceData.loadWorkspaces.mockResolvedValue(mockWorkspaces);
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.switchWorkspace('new-workspace');
});
expect(mockUpdateLastWorkspaceName).toHaveBeenCalledWith('new-workspace');
expect(mockWorkspaceData.loadWorkspaceData).toHaveBeenCalledWith(
'new-workspace'
);
expect(mockWorkspaceData.loadWorkspaces).toHaveBeenCalled();
});
it('handles switch workspace errors', async () => {
const mockUpdateLastWorkspaceName = vi.mocked(
workspaceApi.updateLastWorkspaceName
);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockUpdateLastWorkspaceName.mockRejectedValue(new Error('Switch failed'));
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.switchWorkspace('error-workspace');
});
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to switch workspace:',
expect.any(Error)
);
expect(notifications.show).toHaveBeenCalledWith({
title: 'Error',
message: 'Failed to switch workspace',
color: 'red',
});
consoleSpy.mockRestore();
});
it('handles load workspace data errors during switch', async () => {
const mockUpdateLastWorkspaceName = vi.mocked(
workspaceApi.updateLastWorkspaceName
);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockUpdateLastWorkspaceName.mockResolvedValue(undefined);
mockWorkspaceData.loadWorkspaceData.mockRejectedValue(
new Error('Load failed')
);
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.switchWorkspace('error-workspace');
});
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to switch workspace:',
expect.any(Error)
);
expect(notifications.show).toHaveBeenCalledWith({
title: 'Error',
message: 'Failed to switch workspace',
color: 'red',
});
consoleSpy.mockRestore();
});
});
describe('deleteCurrentWorkspace', () => {
it('deletes workspace successfully when multiple workspaces exist', async () => {
const mockDeleteWorkspace = vi.mocked(workspaceApi.deleteWorkspace);
mockDeleteWorkspace.mockResolvedValue('next-workspace');
mockWorkspaceData.loadWorkspaces.mockResolvedValue(mockWorkspaces);
mockWorkspaceData.loadWorkspaceData.mockResolvedValue(undefined);
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.deleteCurrentWorkspace();
});
expect(mockWorkspaceData.loadWorkspaces).toHaveBeenCalledTimes(2); // Once for check, once after deletion
expect(mockDeleteWorkspace).toHaveBeenCalledWith('test-workspace');
expect(mockWorkspaceData.loadWorkspaceData).toHaveBeenCalledWith(
'next-workspace'
);
expect(notifications.show).toHaveBeenCalledWith({
title: 'Success',
message: 'Workspace deleted successfully',
color: 'green',
});
});
it('prevents deletion when only one workspace exists', async () => {
const singleWorkspace = [mockWorkspaces[0]].filter(
(w): w is Workspace => w !== undefined
);
mockWorkspaceData.loadWorkspaces.mockResolvedValue(singleWorkspace);
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.deleteCurrentWorkspace();
});
expect(workspaceApi.deleteWorkspace).not.toHaveBeenCalled();
expect(notifications.show).toHaveBeenCalledWith({
title: 'Error',
message:
'Cannot delete the last workspace. At least one workspace must exist.',
color: 'red',
});
});
it('does nothing when no current workspace', async () => {
mockWorkspaceData.currentWorkspace = null;
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.deleteCurrentWorkspace();
});
expect(workspaceApi.deleteWorkspace).not.toHaveBeenCalled();
expect(mockWorkspaceData.loadWorkspaces).not.toHaveBeenCalled();
});
it('handles delete workspace API errors', async () => {
const mockDeleteWorkspace = vi.mocked(workspaceApi.deleteWorkspace);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockWorkspaceData.loadWorkspaces.mockResolvedValue(mockWorkspaces);
mockDeleteWorkspace.mockRejectedValue(new Error('Delete failed'));
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.deleteCurrentWorkspace();
});
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to delete workspace:',
expect.any(Error)
);
expect(notifications.show).toHaveBeenCalledWith({
title: 'Error',
message: 'Failed to delete workspace',
color: 'red',
});
consoleSpy.mockRestore();
});
});
describe('updateSettings', () => {
it('updates workspace settings successfully', async () => {
const mockUpdateWorkspace = vi.mocked(workspaceApi.updateWorkspace);
const updatedWorkspace: Workspace = {
...mockWorkspaceData.currentWorkspace!,
autoSave: true,
showHiddenFiles: true,
};
mockUpdateWorkspace.mockResolvedValue(updatedWorkspace);
mockWorkspaceData.loadWorkspaces.mockResolvedValue(mockWorkspaces);
const { result } = renderHook(() => useWorkspaceOperations());
const newSettings = {
autoSave: true,
showHiddenFiles: true,
};
await act(async () => {
await result.current.updateSettings(newSettings);
});
expect(mockUpdateWorkspace).toHaveBeenCalledWith('test-workspace', {
...mockWorkspaceData.currentWorkspace,
...newSettings,
});
expect(mockWorkspaceData.setCurrentWorkspace).toHaveBeenCalledWith(
updatedWorkspace
);
expect(mockWorkspaceData.loadWorkspaces).toHaveBeenCalled();
});
it('updates theme and calls updateColorScheme', async () => {
const mockUpdateWorkspace = vi.mocked(workspaceApi.updateWorkspace);
const updatedWorkspace: Workspace = {
...mockWorkspaceData.currentWorkspace!,
theme: Theme.Dark,
};
mockUpdateWorkspace.mockResolvedValue(updatedWorkspace);
mockWorkspaceData.loadWorkspaces.mockResolvedValue(mockWorkspaces);
const { result } = renderHook(() => useWorkspaceOperations());
const newSettings = {
theme: Theme.Dark,
};
await act(async () => {
await result.current.updateSettings(newSettings);
});
expect(mockUpdateWorkspace).toHaveBeenCalledWith('test-workspace', {
...mockWorkspaceData.currentWorkspace,
theme: Theme.Dark,
});
expect(mockTheme.updateColorScheme).toHaveBeenCalledWith(Theme.Dark);
expect(mockWorkspaceData.setCurrentWorkspace).toHaveBeenCalledWith(
updatedWorkspace
);
});
it('updates multiple settings including theme', async () => {
const mockUpdateWorkspace = vi.mocked(workspaceApi.updateWorkspace);
const updatedWorkspace: Workspace = {
...mockWorkspaceData.currentWorkspace!,
theme: Theme.Dark,
autoSave: true,
gitEnabled: true,
gitUrl: 'https://github.com/user/repo.git',
};
mockUpdateWorkspace.mockResolvedValue(updatedWorkspace);
mockWorkspaceData.loadWorkspaces.mockResolvedValue(mockWorkspaces);
const { result } = renderHook(() => useWorkspaceOperations());
const newSettings = {
theme: Theme.Dark,
autoSave: true,
gitEnabled: true,
gitUrl: 'https://github.com/user/repo.git',
};
await act(async () => {
await result.current.updateSettings(newSettings);
});
expect(mockUpdateWorkspace).toHaveBeenCalledWith('test-workspace', {
...mockWorkspaceData.currentWorkspace,
...newSettings,
});
expect(mockTheme.updateColorScheme).toHaveBeenCalledWith(Theme.Dark);
expect(mockWorkspaceData.setCurrentWorkspace).toHaveBeenCalledWith(
updatedWorkspace
);
expect(mockWorkspaceData.loadWorkspaces).toHaveBeenCalled();
});
it('does nothing when no current workspace', async () => {
mockWorkspaceData.currentWorkspace = null;
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.updateSettings({ autoSave: true });
});
expect(workspaceApi.updateWorkspace).not.toHaveBeenCalled();
expect(mockWorkspaceData.setCurrentWorkspace).not.toHaveBeenCalled();
expect(mockWorkspaceData.loadWorkspaces).not.toHaveBeenCalled();
});
it('handles update workspace API errors', async () => {
const mockUpdateWorkspace = vi.mocked(workspaceApi.updateWorkspace);
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockUpdateWorkspace.mockRejectedValue(new Error('Update failed'));
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
try {
await result.current.updateSettings({ autoSave: true });
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe('Update failed');
}
});
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to save settings:',
expect.any(Error)
);
consoleSpy.mockRestore();
});
it('handles empty settings update', async () => {
const mockUpdateWorkspace = vi.mocked(workspaceApi.updateWorkspace);
const updatedWorkspace = mockWorkspaceData.currentWorkspace!;
mockUpdateWorkspace.mockResolvedValue(updatedWorkspace);
mockWorkspaceData.loadWorkspaces.mockResolvedValue(mockWorkspaces);
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.updateSettings({});
});
expect(mockUpdateWorkspace).toHaveBeenCalledWith(
'test-workspace',
mockWorkspaceData.currentWorkspace
);
expect(mockWorkspaceData.setCurrentWorkspace).toHaveBeenCalledWith(
updatedWorkspace
);
});
});
describe('hook interface', () => {
it('returns correct function interface', () => {
const { result } = renderHook(() => useWorkspaceOperations());
expect(typeof result.current.switchWorkspace).toBe('function');
expect(typeof result.current.deleteCurrentWorkspace).toBe('function');
expect(typeof result.current.updateSettings).toBe('function');
});
it('functions are stable across re-renders', () => {
const { result, rerender } = renderHook(() => useWorkspaceOperations());
const initialFunctions = {
switchWorkspace: result.current.switchWorkspace,
deleteCurrentWorkspace: result.current.deleteCurrentWorkspace,
updateSettings: result.current.updateSettings,
};
rerender();
expect(result.current.switchWorkspace).toBe(
initialFunctions.switchWorkspace
);
expect(result.current.deleteCurrentWorkspace).toBe(
initialFunctions.deleteCurrentWorkspace
);
expect(result.current.updateSettings).toBe(
initialFunctions.updateSettings
);
});
});
describe('workspace data integration', () => {
it('uses current workspace name for API calls', async () => {
const mockUpdateWorkspace = vi.mocked(workspaceApi.updateWorkspace);
const mockDeleteWorkspace = vi.mocked(workspaceApi.deleteWorkspace);
// Update workspace name
mockWorkspaceData.currentWorkspace = {
...mockWorkspaceData.currentWorkspace!,
name: 'different-workspace',
};
mockUpdateWorkspace.mockResolvedValue(mockWorkspaceData.currentWorkspace);
mockDeleteWorkspace.mockResolvedValue('next-workspace');
mockWorkspaceData.loadWorkspaces.mockResolvedValue(mockWorkspaces);
mockWorkspaceData.loadWorkspaceData.mockResolvedValue(undefined);
const { result } = renderHook(() => useWorkspaceOperations());
// Test update settings
await act(async () => {
await result.current.updateSettings({ autoSave: true });
});
expect(mockUpdateWorkspace).toHaveBeenCalledWith(
'different-workspace',
expect.any(Object)
);
// Test delete workspace
await act(async () => {
await result.current.deleteCurrentWorkspace();
});
expect(mockDeleteWorkspace).toHaveBeenCalledWith('different-workspace');
});
it('handles workspace changes during operations', () => {
const { result, rerender } = renderHook(() => useWorkspaceOperations());
// Change workspace
mockWorkspaceData.currentWorkspace = {
...mockWorkspaceData.currentWorkspace!,
name: 'new-workspace',
};
rerender();
// Functions should still work with new workspace
expect(typeof result.current.switchWorkspace).toBe('function');
expect(typeof result.current.deleteCurrentWorkspace).toBe('function');
expect(typeof result.current.updateSettings).toBe('function');
});
});
describe('concurrent operations', () => {
it('handles update settings after switch workspace', async () => {
const mockUpdateLastWorkspaceName = vi.mocked(
workspaceApi.updateLastWorkspaceName
);
const mockUpdateWorkspace = vi.mocked(workspaceApi.updateWorkspace);
mockUpdateLastWorkspaceName.mockResolvedValue(undefined);
mockWorkspaceData.loadWorkspaceData.mockResolvedValue(undefined);
mockWorkspaceData.loadWorkspaces.mockResolvedValue(mockWorkspaces);
mockUpdateWorkspace.mockResolvedValue(
mockWorkspaceData.currentWorkspace!
);
const { result } = renderHook(() => useWorkspaceOperations());
await act(async () => {
await result.current.switchWorkspace('new-workspace');
});
await act(async () => {
await result.current.updateSettings({ autoSave: true });
});
expect(mockUpdateLastWorkspaceName).toHaveBeenCalledWith('new-workspace');
expect(mockUpdateWorkspace).toHaveBeenCalledWith(
'test-workspace',
expect.objectContaining({ autoSave: true })
);
});
});
});