mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +00:00
Refactor hooks and hook tests error handling and state management
This commit is contained in:
@@ -62,9 +62,7 @@ describe('useFileNavigation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockLastOpenedFile.loadLastOpenedFile).toHaveBeenCalled();
|
expect(mockLastOpenedFile.loadLastOpenedFile).toHaveBeenCalled();
|
||||||
expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledWith(
|
expect(mockLastOpenedFile.saveLastOpenedFile).not.toHaveBeenCalled();
|
||||||
'documents/readme.md'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stays with default file when no last opened file exists', async () => {
|
it('stays with default file when no last opened file exists', async () => {
|
||||||
@@ -90,8 +88,11 @@ describe('useFileNavigation', () => {
|
|||||||
await result.current.handleFileSelect('notes/todo.md');
|
await result.current.handleFileSelect('notes/todo.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.selectedFile).toBe('notes/todo.md');
|
await waitFor(() => {
|
||||||
expect(result.current.isNewFile).toBe(false);
|
expect(result.current.selectedFile).toBe('notes/todo.md');
|
||||||
|
expect(result.current.isNewFile).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledWith(
|
expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledWith(
|
||||||
'notes/todo.md'
|
'notes/todo.md'
|
||||||
);
|
);
|
||||||
@@ -109,7 +110,7 @@ describe('useFileNavigation', () => {
|
|||||||
expect(mockLastOpenedFile.saveLastOpenedFile).not.toHaveBeenCalled();
|
expect(mockLastOpenedFile.saveLastOpenedFile).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles empty string file selection', async () => {
|
it('handles empty string file selection with default file', async () => {
|
||||||
const { result } = renderHook(() => useFileNavigation());
|
const { result } = renderHook(() => useFileNavigation());
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -121,6 +122,32 @@ describe('useFileNavigation', () => {
|
|||||||
expect(mockLastOpenedFile.saveLastOpenedFile).not.toHaveBeenCalled();
|
expect(mockLastOpenedFile.saveLastOpenedFile).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves current selection when passed empty string with existing selection', async () => {
|
||||||
|
const { result } = renderHook(() => useFileNavigation());
|
||||||
|
|
||||||
|
// First select a valid file
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleFileSelect('existing-file.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.selectedFile).toBe('existing-file.md');
|
||||||
|
expect(result.current.isNewFile).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Now send empty string
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleFileSelect('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Selection should be preserved
|
||||||
|
expect(result.current.selectedFile).toBe('existing-file.md');
|
||||||
|
expect(result.current.isNewFile).toBe(false);
|
||||||
|
expect(mockLastOpenedFile.saveLastOpenedFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('handles different file path formats', async () => {
|
it('handles different file path formats', async () => {
|
||||||
const { result } = renderHook(() => useFileNavigation());
|
const { result } = renderHook(() => useFileNavigation());
|
||||||
|
|
||||||
@@ -138,8 +165,11 @@ describe('useFileNavigation', () => {
|
|||||||
await result.current.handleFileSelect(filePath);
|
await result.current.handleFileSelect(filePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.selectedFile).toBe(filePath);
|
await waitFor(() => {
|
||||||
expect(result.current.isNewFile).toBe(false);
|
expect(result.current.selectedFile).toBe(filePath);
|
||||||
|
expect(result.current.isNewFile).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledWith(
|
expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledWith(
|
||||||
filePath
|
filePath
|
||||||
);
|
);
|
||||||
@@ -155,16 +185,22 @@ describe('useFileNavigation', () => {
|
|||||||
|
|
||||||
const files = ['file1.md', 'file2.md', 'file3.md'];
|
const files = ['file1.md', 'file2.md', 'file3.md'];
|
||||||
|
|
||||||
await act(async () => {
|
// Use sequential state updates instead of Promise.all for more predictable results
|
||||||
await Promise.all(
|
for (const file of files) {
|
||||||
files.map((file) => result.current.handleFileSelect(file))
|
await act(async () => {
|
||||||
);
|
await result.current.handleFileSelect(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// After all updates, we should have the last file selected
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.selectedFile).toBe(files[files.length - 1]);
|
||||||
|
expect(result.current.isNewFile).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should end up with the last file (depending on async timing)
|
expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledTimes(
|
||||||
expect(files).toContain(result.current.selectedFile);
|
files.length
|
||||||
expect(result.current.isNewFile).toBe(false);
|
);
|
||||||
expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledTimes(3);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles file selection errors gracefully', async () => {
|
it('handles file selection errors gracefully', async () => {
|
||||||
@@ -179,8 +215,11 @@ describe('useFileNavigation', () => {
|
|||||||
await result.current.handleFileSelect('error-file.md');
|
await result.current.handleFileSelect('error-file.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.selectedFile).toBe('error-file.md');
|
// Wait for state update despite the error
|
||||||
expect(result.current.isNewFile).toBe(false);
|
await waitFor(() => {
|
||||||
|
expect(result.current.selectedFile).toBe('error-file.md');
|
||||||
|
expect(result.current.isNewFile).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -324,15 +363,22 @@ describe('useFileNavigation', () => {
|
|||||||
await result.current.handleFileSelect('real-file.md');
|
await result.current.handleFileSelect('real-file.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.isNewFile).toBe(false);
|
// Wait for state to update
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.selectedFile).toBe('real-file.md');
|
||||||
|
expect(result.current.isNewFile).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
// Go back to null (should default to default file)
|
// Go back to null (should default to default file)
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await result.current.handleFileSelect(null);
|
await result.current.handleFileSelect(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.selectedFile).toBe(DEFAULT_FILE.path);
|
// Wait for state to update again
|
||||||
expect(result.current.isNewFile).toBe(true);
|
await waitFor(() => {
|
||||||
|
expect(result.current.selectedFile).toBe(DEFAULT_FILE.path);
|
||||||
|
expect(result.current.isNewFile).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maintains correct isNewFile state for regular files', async () => {
|
it('maintains correct isNewFile state for regular files', async () => {
|
||||||
@@ -345,8 +391,11 @@ describe('useFileNavigation', () => {
|
|||||||
await result.current.handleFileSelect(file);
|
await result.current.handleFileSelect(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.selectedFile).toBe(file);
|
// Wait for each file selection to complete
|
||||||
expect(result.current.isNewFile).toBe(false);
|
await waitFor(() => {
|
||||||
|
expect(result.current.selectedFile).toBe(file);
|
||||||
|
expect(result.current.isNewFile).toBe(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -414,8 +463,10 @@ describe('useFileNavigation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// State should still be updated despite save error
|
// State should still be updated despite save error
|
||||||
expect(result.current.selectedFile).toBe('test-file.md');
|
await waitFor(() => {
|
||||||
expect(result.current.isNewFile).toBe(false);
|
expect(result.current.selectedFile).toBe('test-file.md');
|
||||||
|
expect(result.current.isNewFile).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,35 +17,55 @@ export const useFileNavigation = (): UseFileNavigationResult => {
|
|||||||
|
|
||||||
const handleFileSelect = useCallback(
|
const handleFileSelect = useCallback(
|
||||||
async (filePath: string | null): Promise<void> => {
|
async (filePath: string | null): Promise<void> => {
|
||||||
const newPath = filePath || DEFAULT_FILE.path;
|
// Consider empty string as null
|
||||||
setSelectedFile(newPath);
|
const effectiveFilePath = filePath === '' ? null : filePath;
|
||||||
setIsNewFile(!filePath);
|
|
||||||
|
|
||||||
if (filePath) {
|
if (effectiveFilePath) {
|
||||||
await saveLastOpenedFile(filePath);
|
setSelectedFile(effectiveFilePath);
|
||||||
|
setIsNewFile(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to save the last opened file
|
||||||
|
await saveLastOpenedFile(effectiveFilePath);
|
||||||
|
} catch (err) {
|
||||||
|
// Silently handle the error so state still updates
|
||||||
|
console.error('Failed to save last opened file:', err);
|
||||||
|
}
|
||||||
|
} else if (selectedFile === DEFAULT_FILE.path || filePath === null) {
|
||||||
|
setSelectedFile(DEFAULT_FILE.path);
|
||||||
|
setIsNewFile(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[saveLastOpenedFile]
|
[saveLastOpenedFile, selectedFile]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load last opened file when workspace changes
|
// Load last opened file when workspace changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeFile = async (): Promise<void> => {
|
const initializeFile = async (): Promise<void> => {
|
||||||
setSelectedFile(DEFAULT_FILE.path);
|
try {
|
||||||
setIsNewFile(true);
|
setSelectedFile(DEFAULT_FILE.path);
|
||||||
|
setIsNewFile(true);
|
||||||
|
|
||||||
const lastFile = await loadLastOpenedFile();
|
const lastFile = await loadLastOpenedFile();
|
||||||
if (lastFile) {
|
|
||||||
await handleFileSelect(lastFile);
|
if (lastFile) {
|
||||||
} else {
|
setSelectedFile(lastFile);
|
||||||
await handleFileSelect(null);
|
setIsNewFile(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load last opened file:', err);
|
||||||
|
setSelectedFile(DEFAULT_FILE.path);
|
||||||
|
setIsNewFile(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentWorkspace) {
|
if (currentWorkspace) {
|
||||||
void initializeFile();
|
void initializeFile();
|
||||||
|
} else {
|
||||||
|
setSelectedFile(DEFAULT_FILE.path);
|
||||||
|
setIsNewFile(true);
|
||||||
}
|
}
|
||||||
}, [currentWorkspace, loadLastOpenedFile, handleFileSelect]);
|
}, [currentWorkspace, loadLastOpenedFile, saveLastOpenedFile]);
|
||||||
|
|
||||||
return { selectedFile, isNewFile, handleFileSelect };
|
return { selectedFile, isNewFile, handleFileSelect };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
deleteUser as adminDeleteUser,
|
deleteUser as adminDeleteUser,
|
||||||
} from '../api/admin';
|
} from '../api/admin';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { useCallback } from 'react';
|
||||||
import type { User } from '@/types/models';
|
import type { User } from '@/types/models';
|
||||||
import type { CreateUserRequest, UpdateUserRequest } from '@/types/api';
|
import type { CreateUserRequest, UpdateUserRequest } from '@/types/api';
|
||||||
|
|
||||||
@@ -20,73 +21,77 @@ interface UseUserAdminResult {
|
|||||||
export const useUserAdmin = (): UseUserAdminResult => {
|
export const useUserAdmin = (): UseUserAdminResult => {
|
||||||
const { data: users, loading, error, reload } = useAdminData('users');
|
const { data: users, loading, error, reload } = useAdminData('users');
|
||||||
|
|
||||||
const handleCreate = async (
|
const handleCreate = useCallback(
|
||||||
userData: CreateUserRequest
|
async (userData: CreateUserRequest): Promise<boolean> => {
|
||||||
): Promise<boolean> => {
|
try {
|
||||||
try {
|
await createUser(userData);
|
||||||
await createUser(userData);
|
notifications.show({
|
||||||
notifications.show({
|
title: 'Success',
|
||||||
title: 'Success',
|
message: 'User created successfully',
|
||||||
message: 'User created successfully',
|
color: 'green',
|
||||||
color: 'green',
|
});
|
||||||
});
|
await reload();
|
||||||
await reload();
|
return true;
|
||||||
return true;
|
} catch (err) {
|
||||||
} catch (err) {
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
notifications.show({
|
||||||
notifications.show({
|
title: 'Error',
|
||||||
title: 'Error',
|
message: `Failed to create user: ${message}`,
|
||||||
message: `Failed to create user: ${message}`,
|
color: 'red',
|
||||||
color: 'red',
|
});
|
||||||
});
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
},
|
||||||
};
|
[reload]
|
||||||
|
);
|
||||||
|
|
||||||
const handleUpdate = async (
|
const handleUpdate = useCallback(
|
||||||
userId: number,
|
async (userId: number, userData: UpdateUserRequest): Promise<boolean> => {
|
||||||
userData: UpdateUserRequest
|
try {
|
||||||
): Promise<boolean> => {
|
await updateUser(userId, userData);
|
||||||
try {
|
notifications.show({
|
||||||
await updateUser(userId, userData);
|
title: 'Success',
|
||||||
notifications.show({
|
message: 'User updated successfully',
|
||||||
title: 'Success',
|
color: 'green',
|
||||||
message: 'User updated successfully',
|
});
|
||||||
color: 'green',
|
await reload();
|
||||||
});
|
return true;
|
||||||
await reload();
|
} catch (err) {
|
||||||
return true;
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
} catch (err) {
|
notifications.show({
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
title: 'Error',
|
||||||
notifications.show({
|
message: `Failed to update user: ${message}`,
|
||||||
title: 'Error',
|
color: 'red',
|
||||||
message: `Failed to update user: ${message}`,
|
});
|
||||||
color: 'red',
|
return false;
|
||||||
});
|
}
|
||||||
return false;
|
},
|
||||||
}
|
[reload]
|
||||||
};
|
);
|
||||||
|
|
||||||
const handleDelete = async (userId: number): Promise<boolean> => {
|
const handleDelete = useCallback(
|
||||||
try {
|
async (userId: number): Promise<boolean> => {
|
||||||
await adminDeleteUser(userId);
|
try {
|
||||||
notifications.show({
|
await adminDeleteUser(userId);
|
||||||
title: 'Success',
|
notifications.show({
|
||||||
message: 'User deleted successfully',
|
title: 'Success',
|
||||||
color: 'green',
|
message: 'User deleted successfully',
|
||||||
});
|
color: 'green',
|
||||||
await reload();
|
});
|
||||||
return true;
|
await reload();
|
||||||
} catch (err) {
|
return true;
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
} catch (err) {
|
||||||
notifications.show({
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
title: 'Error',
|
notifications.show({
|
||||||
message: `Failed to delete user: ${message}`,
|
title: 'Error',
|
||||||
color: 'red',
|
message: `Failed to delete user: ${message}`,
|
||||||
});
|
color: 'red',
|
||||||
return false;
|
});
|
||||||
}
|
return false;
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[reload]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users,
|
users,
|
||||||
|
|||||||
548
app/src/hooks/useWorkspace.test.ts
Normal file
548
app/src/hooks/useWorkspace.test.ts
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useWorkspace } from './useWorkspace';
|
||||||
|
import {
|
||||||
|
Theme,
|
||||||
|
type Workspace,
|
||||||
|
DEFAULT_WORKSPACE_SETTINGS,
|
||||||
|
} from '@/types/models';
|
||||||
|
import type { MantineColorScheme } from '@mantine/core';
|
||||||
|
|
||||||
|
// Mock the constituent hooks
|
||||||
|
const mockWorkspaceData = {
|
||||||
|
currentWorkspace: null as Workspace | null,
|
||||||
|
workspaces: [] as Workspace[],
|
||||||
|
settings: DEFAULT_WORKSPACE_SETTINGS,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockTheme = {
|
||||||
|
colorScheme: 'light' as MantineColorScheme,
|
||||||
|
updateColorScheme: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockWorkspaceOperations = {
|
||||||
|
switchWorkspace: vi.fn(),
|
||||||
|
deleteCurrentWorkspace: vi.fn(),
|
||||||
|
updateSettings: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('../contexts/WorkspaceDataContext', () => ({
|
||||||
|
useWorkspaceData: () => mockWorkspaceData,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../contexts/ThemeContext', () => ({
|
||||||
|
useTheme: () => mockTheme,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./useWorkspaceOperations', () => ({
|
||||||
|
useWorkspaceOperations: () => mockWorkspaceOperations,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock workspace data
|
||||||
|
const mockWorkspace: Workspace = {
|
||||||
|
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: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockWorkspaces: Workspace[] = [
|
||||||
|
mockWorkspace,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
userId: 1,
|
||||||
|
name: 'second-workspace',
|
||||||
|
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('useWorkspace', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
// Reset mock data to defaults
|
||||||
|
mockWorkspaceData.currentWorkspace = null;
|
||||||
|
mockWorkspaceData.workspaces = [];
|
||||||
|
mockWorkspaceData.settings = DEFAULT_WORKSPACE_SETTINGS;
|
||||||
|
mockWorkspaceData.loading = false;
|
||||||
|
mockTheme.colorScheme = 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initial state', () => {
|
||||||
|
it('returns default values when no workspace is loaded', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.currentWorkspace).toBeNull();
|
||||||
|
expect(result.current.workspaces).toEqual([]);
|
||||||
|
expect(result.current.settings).toEqual(DEFAULT_WORKSPACE_SETTINGS);
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
expect(result.current.colorScheme).toBe('light');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides all expected functions', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(typeof result.current.updateSettings).toBe('function');
|
||||||
|
expect(typeof result.current.updateColorScheme).toBe('function');
|
||||||
|
expect(typeof result.current.switchWorkspace).toBe('function');
|
||||||
|
expect(typeof result.current.deleteCurrentWorkspace).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('workspace data integration', () => {
|
||||||
|
it('returns current workspace data', () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = mockWorkspace;
|
||||||
|
mockWorkspaceData.workspaces = mockWorkspaces;
|
||||||
|
mockWorkspaceData.settings = mockWorkspace;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.currentWorkspace).toEqual(mockWorkspace);
|
||||||
|
expect(result.current.workspaces).toEqual(mockWorkspaces);
|
||||||
|
expect(result.current.settings).toEqual(mockWorkspace);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns loading state from workspace data', () => {
|
||||||
|
mockWorkspaceData.loading = true;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default settings when no current workspace', () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = null;
|
||||||
|
mockWorkspaceData.settings = DEFAULT_WORKSPACE_SETTINGS;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.settings).toEqual(DEFAULT_WORKSPACE_SETTINGS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses current workspace as settings when available', () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = mockWorkspace;
|
||||||
|
mockWorkspaceData.settings = mockWorkspace;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.settings).toEqual(mockWorkspace);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('theme integration', () => {
|
||||||
|
it('returns color scheme from theme context', () => {
|
||||||
|
mockTheme.colorScheme = 'dark';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.colorScheme).toBe('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides updateColorScheme function from theme context', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.updateColorScheme).toBe(
|
||||||
|
mockTheme.updateColorScheme
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles light theme', () => {
|
||||||
|
mockTheme.colorScheme = 'light';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.colorScheme).toBe('light');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles auto theme', () => {
|
||||||
|
mockTheme.colorScheme = 'auto';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.colorScheme).toBe('auto');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('workspace operations integration', () => {
|
||||||
|
it('provides switchWorkspace function from operations', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.switchWorkspace).toBe(
|
||||||
|
mockWorkspaceOperations.switchWorkspace
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides deleteCurrentWorkspace function from operations', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.deleteCurrentWorkspace).toBe(
|
||||||
|
mockWorkspaceOperations.deleteCurrentWorkspace
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides updateSettings function from operations', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.updateSettings).toBe(
|
||||||
|
mockWorkspaceOperations.updateSettings
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('data consistency', () => {
|
||||||
|
it('returns consistent data across multiple renders', () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = mockWorkspace;
|
||||||
|
mockWorkspaceData.workspaces = mockWorkspaces;
|
||||||
|
mockWorkspaceData.settings = mockWorkspace;
|
||||||
|
mockTheme.colorScheme = 'dark';
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
const firstResult = { ...result.current };
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.currentWorkspace).toEqual(
|
||||||
|
firstResult.currentWorkspace
|
||||||
|
);
|
||||||
|
expect(result.current.workspaces).toEqual(firstResult.workspaces);
|
||||||
|
expect(result.current.settings).toEqual(firstResult.settings);
|
||||||
|
expect(result.current.colorScheme).toEqual(firstResult.colorScheme);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects changes in underlying data', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
// Initially no workspace
|
||||||
|
expect(result.current.currentWorkspace).toBeNull();
|
||||||
|
expect(result.current.workspaces).toEqual([]);
|
||||||
|
|
||||||
|
// Add workspace data
|
||||||
|
mockWorkspaceData.currentWorkspace = mockWorkspace;
|
||||||
|
mockWorkspaceData.workspaces = mockWorkspaces;
|
||||||
|
mockWorkspaceData.settings = mockWorkspace;
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.currentWorkspace).toEqual(mockWorkspace);
|
||||||
|
expect(result.current.workspaces).toEqual(mockWorkspaces);
|
||||||
|
expect(result.current.settings).toEqual(mockWorkspace);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects theme changes', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
// Initially light theme
|
||||||
|
expect(result.current.colorScheme).toBe('light');
|
||||||
|
|
||||||
|
// Change to dark theme
|
||||||
|
mockTheme.colorScheme = 'dark';
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.colorScheme).toBe('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects loading state changes', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
// Initially not loading
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
|
||||||
|
// Change to loading
|
||||||
|
mockWorkspaceData.loading = true;
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('function stability', () => {
|
||||||
|
it('maintains stable function references across re-renders', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
const initialFunctions = {
|
||||||
|
updateSettings: result.current.updateSettings,
|
||||||
|
updateColorScheme: result.current.updateColorScheme,
|
||||||
|
switchWorkspace: result.current.switchWorkspace,
|
||||||
|
deleteCurrentWorkspace: result.current.deleteCurrentWorkspace,
|
||||||
|
};
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.updateSettings).toBe(
|
||||||
|
initialFunctions.updateSettings
|
||||||
|
);
|
||||||
|
expect(result.current.updateColorScheme).toBe(
|
||||||
|
initialFunctions.updateColorScheme
|
||||||
|
);
|
||||||
|
expect(result.current.switchWorkspace).toBe(
|
||||||
|
initialFunctions.switchWorkspace
|
||||||
|
);
|
||||||
|
expect(result.current.deleteCurrentWorkspace).toBe(
|
||||||
|
initialFunctions.deleteCurrentWorkspace
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maintains stable function references when data changes', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
const initialFunctions = {
|
||||||
|
updateSettings: result.current.updateSettings,
|
||||||
|
updateColorScheme: result.current.updateColorScheme,
|
||||||
|
switchWorkspace: result.current.switchWorkspace,
|
||||||
|
deleteCurrentWorkspace: result.current.deleteCurrentWorkspace,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change data
|
||||||
|
mockWorkspaceData.currentWorkspace = mockWorkspace;
|
||||||
|
mockWorkspaceData.workspaces = mockWorkspaces;
|
||||||
|
mockTheme.colorScheme = 'dark';
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.updateSettings).toBe(
|
||||||
|
initialFunctions.updateSettings
|
||||||
|
);
|
||||||
|
expect(result.current.updateColorScheme).toBe(
|
||||||
|
initialFunctions.updateColorScheme
|
||||||
|
);
|
||||||
|
expect(result.current.switchWorkspace).toBe(
|
||||||
|
initialFunctions.switchWorkspace
|
||||||
|
);
|
||||||
|
expect(result.current.deleteCurrentWorkspace).toBe(
|
||||||
|
initialFunctions.deleteCurrentWorkspace
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hook interface', () => {
|
||||||
|
it('returns correct interface structure', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
const expectedKeys = [
|
||||||
|
'currentWorkspace',
|
||||||
|
'workspaces',
|
||||||
|
'settings',
|
||||||
|
'updateSettings',
|
||||||
|
'loading',
|
||||||
|
'colorScheme',
|
||||||
|
'updateColorScheme',
|
||||||
|
'switchWorkspace',
|
||||||
|
'deleteCurrentWorkspace',
|
||||||
|
];
|
||||||
|
|
||||||
|
expectedKeys.forEach((key) => {
|
||||||
|
expect(key in result.current).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct types for all properties', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
result.current.currentWorkspace === null ||
|
||||||
|
typeof result.current.currentWorkspace === 'object'
|
||||||
|
).toBe(true);
|
||||||
|
expect(Array.isArray(result.current.workspaces)).toBe(true);
|
||||||
|
expect(typeof result.current.settings === 'object').toBe(true);
|
||||||
|
expect(typeof result.current.updateSettings === 'function').toBe(true);
|
||||||
|
expect(typeof result.current.loading === 'boolean').toBe(true);
|
||||||
|
expect(typeof result.current.colorScheme === 'string').toBe(true);
|
||||||
|
expect(typeof result.current.updateColorScheme === 'function').toBe(true);
|
||||||
|
expect(typeof result.current.switchWorkspace === 'function').toBe(true);
|
||||||
|
expect(typeof result.current.deleteCurrentWorkspace === 'function').toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('handles undefined workspace data gracefully', () => {
|
||||||
|
// Simulate undefined data that might occur during loading
|
||||||
|
mockWorkspaceData.currentWorkspace = null;
|
||||||
|
mockWorkspaceData.workspaces = [];
|
||||||
|
mockWorkspaceData.settings = DEFAULT_WORKSPACE_SETTINGS;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.currentWorkspace).toBeNull();
|
||||||
|
expect(result.current.workspaces).toEqual([]);
|
||||||
|
expect(result.current.settings).toEqual(DEFAULT_WORKSPACE_SETTINGS);
|
||||||
|
expect(typeof result.current.updateSettings).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty workspaces array', () => {
|
||||||
|
mockWorkspaceData.workspaces = [];
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.workspaces).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles single workspace', () => {
|
||||||
|
const singleWorkspace = [mockWorkspace];
|
||||||
|
mockWorkspaceData.workspaces = singleWorkspace;
|
||||||
|
mockWorkspaceData.currentWorkspace = mockWorkspace;
|
||||||
|
mockWorkspaceData.settings = mockWorkspace;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.workspaces).toEqual(singleWorkspace);
|
||||||
|
expect(result.current.currentWorkspace).toEqual(mockWorkspace);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles workspace with minimal data', () => {
|
||||||
|
const minimalWorkspace: Workspace = {
|
||||||
|
name: 'minimal',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
...DEFAULT_WORKSPACE_SETTINGS,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockWorkspaceData.currentWorkspace = minimalWorkspace;
|
||||||
|
mockWorkspaceData.settings = minimalWorkspace;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.currentWorkspace).toEqual(minimalWorkspace);
|
||||||
|
expect(result.current.settings).toEqual(minimalWorkspace);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('integration scenarios', () => {
|
||||||
|
it('provides complete workspace management interface', () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = mockWorkspace;
|
||||||
|
mockWorkspaceData.workspaces = mockWorkspaces;
|
||||||
|
mockWorkspaceData.settings = mockWorkspace;
|
||||||
|
mockTheme.colorScheme = 'light';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
// Should have all data
|
||||||
|
expect(result.current.currentWorkspace).toEqual(mockWorkspace);
|
||||||
|
expect(result.current.workspaces).toEqual(mockWorkspaces);
|
||||||
|
expect(result.current.settings).toEqual(mockWorkspace);
|
||||||
|
expect(result.current.colorScheme).toBe('light');
|
||||||
|
|
||||||
|
// Should have all operations
|
||||||
|
expect(typeof result.current.updateSettings).toBe('function');
|
||||||
|
expect(typeof result.current.switchWorkspace).toBe('function');
|
||||||
|
expect(typeof result.current.deleteCurrentWorkspace).toBe('function');
|
||||||
|
expect(typeof result.current.updateColorScheme).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports workspace switching workflow', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
// Initially no workspace
|
||||||
|
expect(result.current.currentWorkspace).toBeNull();
|
||||||
|
|
||||||
|
// Should provide switch function
|
||||||
|
expect(typeof result.current.switchWorkspace).toBe('function');
|
||||||
|
expect(result.current.switchWorkspace).toBe(
|
||||||
|
mockWorkspaceOperations.switchWorkspace
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports settings management workflow', () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = mockWorkspace;
|
||||||
|
mockWorkspaceData.settings = mockWorkspace;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
// Should have current settings
|
||||||
|
expect(result.current.settings).toEqual(mockWorkspace);
|
||||||
|
|
||||||
|
// Should provide update function
|
||||||
|
expect(typeof result.current.updateSettings).toBe('function');
|
||||||
|
expect(result.current.updateSettings).toBe(
|
||||||
|
mockWorkspaceOperations.updateSettings
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports theme management workflow', () => {
|
||||||
|
mockTheme.colorScheme = 'dark';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
// Should have current color scheme
|
||||||
|
expect(result.current.colorScheme).toBe('dark');
|
||||||
|
|
||||||
|
// Should provide update function
|
||||||
|
expect(typeof result.current.updateColorScheme).toBe('function');
|
||||||
|
expect(result.current.updateColorScheme).toBe(
|
||||||
|
mockTheme.updateColorScheme
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mock integration validation', () => {
|
||||||
|
it('correctly integrates with WorkspaceDataContext mock', () => {
|
||||||
|
mockWorkspaceData.currentWorkspace = mockWorkspace;
|
||||||
|
mockWorkspaceData.workspaces = mockWorkspaces;
|
||||||
|
mockWorkspaceData.settings = mockWorkspace;
|
||||||
|
mockWorkspaceData.loading = true;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.currentWorkspace).toBe(
|
||||||
|
mockWorkspaceData.currentWorkspace
|
||||||
|
);
|
||||||
|
expect(result.current.workspaces).toBe(mockWorkspaceData.workspaces);
|
||||||
|
expect(result.current.settings).toBe(mockWorkspaceData.settings);
|
||||||
|
expect(result.current.loading).toBe(mockWorkspaceData.loading);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly integrates with ThemeContext mock', () => {
|
||||||
|
mockTheme.colorScheme = 'dark';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.colorScheme).toBe(mockTheme.colorScheme);
|
||||||
|
expect(result.current.updateColorScheme).toBe(
|
||||||
|
mockTheme.updateColorScheme
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly integrates with useWorkspaceOperations mock', () => {
|
||||||
|
const { result } = renderHook(() => useWorkspace());
|
||||||
|
|
||||||
|
expect(result.current.switchWorkspace).toBe(
|
||||||
|
mockWorkspaceOperations.switchWorkspace
|
||||||
|
);
|
||||||
|
expect(result.current.deleteCurrentWorkspace).toBe(
|
||||||
|
mockWorkspaceOperations.deleteCurrentWorkspace
|
||||||
|
);
|
||||||
|
expect(result.current.updateSettings).toBe(
|
||||||
|
mockWorkspaceOperations.updateSettings
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user