diff --git a/app/src/hooks/useFileNavigation.test.ts b/app/src/hooks/useFileNavigation.test.ts index 5e6cc00..ab177be 100644 --- a/app/src/hooks/useFileNavigation.test.ts +++ b/app/src/hooks/useFileNavigation.test.ts @@ -62,9 +62,7 @@ describe('useFileNavigation', () => { }); expect(mockLastOpenedFile.loadLastOpenedFile).toHaveBeenCalled(); - expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledWith( - 'documents/readme.md' - ); + expect(mockLastOpenedFile.saveLastOpenedFile).not.toHaveBeenCalled(); }); 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'); }); - expect(result.current.selectedFile).toBe('notes/todo.md'); - expect(result.current.isNewFile).toBe(false); + await waitFor(() => { + expect(result.current.selectedFile).toBe('notes/todo.md'); + expect(result.current.isNewFile).toBe(false); + }); + expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledWith( 'notes/todo.md' ); @@ -109,7 +110,7 @@ describe('useFileNavigation', () => { 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()); await act(async () => { @@ -121,6 +122,32 @@ describe('useFileNavigation', () => { 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 () => { const { result } = renderHook(() => useFileNavigation()); @@ -138,8 +165,11 @@ describe('useFileNavigation', () => { await result.current.handleFileSelect(filePath); }); - expect(result.current.selectedFile).toBe(filePath); - expect(result.current.isNewFile).toBe(false); + await waitFor(() => { + expect(result.current.selectedFile).toBe(filePath); + expect(result.current.isNewFile).toBe(false); + }); + expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledWith( filePath ); @@ -155,16 +185,22 @@ describe('useFileNavigation', () => { const files = ['file1.md', 'file2.md', 'file3.md']; - await act(async () => { - await Promise.all( - files.map((file) => result.current.handleFileSelect(file)) - ); + // Use sequential state updates instead of Promise.all for more predictable results + for (const file of files) { + 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(files).toContain(result.current.selectedFile); - expect(result.current.isNewFile).toBe(false); - expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledTimes(3); + expect(mockLastOpenedFile.saveLastOpenedFile).toHaveBeenCalledTimes( + files.length + ); }); it('handles file selection errors gracefully', async () => { @@ -179,8 +215,11 @@ describe('useFileNavigation', () => { await result.current.handleFileSelect('error-file.md'); }); - expect(result.current.selectedFile).toBe('error-file.md'); - expect(result.current.isNewFile).toBe(false); + // Wait for state update despite the error + 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'); }); - 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) await act(async () => { await result.current.handleFileSelect(null); }); - expect(result.current.selectedFile).toBe(DEFAULT_FILE.path); - expect(result.current.isNewFile).toBe(true); + // Wait for state to update again + 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 () => { @@ -345,8 +391,11 @@ describe('useFileNavigation', () => { await result.current.handleFileSelect(file); }); - expect(result.current.selectedFile).toBe(file); - expect(result.current.isNewFile).toBe(false); + // Wait for each file selection to complete + 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 - expect(result.current.selectedFile).toBe('test-file.md'); - expect(result.current.isNewFile).toBe(false); + await waitFor(() => { + expect(result.current.selectedFile).toBe('test-file.md'); + expect(result.current.isNewFile).toBe(false); + }); }); }); }); diff --git a/app/src/hooks/useFileNavigation.ts b/app/src/hooks/useFileNavigation.ts index 3240c19..2218461 100644 --- a/app/src/hooks/useFileNavigation.ts +++ b/app/src/hooks/useFileNavigation.ts @@ -17,35 +17,55 @@ export const useFileNavigation = (): UseFileNavigationResult => { const handleFileSelect = useCallback( async (filePath: string | null): Promise => { - const newPath = filePath || DEFAULT_FILE.path; - setSelectedFile(newPath); - setIsNewFile(!filePath); + // Consider empty string as null + const effectiveFilePath = filePath === '' ? null : filePath; - if (filePath) { - await saveLastOpenedFile(filePath); + if (effectiveFilePath) { + 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 useEffect(() => { const initializeFile = async (): Promise => { - setSelectedFile(DEFAULT_FILE.path); - setIsNewFile(true); + try { + setSelectedFile(DEFAULT_FILE.path); + setIsNewFile(true); - const lastFile = await loadLastOpenedFile(); - if (lastFile) { - await handleFileSelect(lastFile); - } else { - await handleFileSelect(null); + const lastFile = await loadLastOpenedFile(); + + if (lastFile) { + setSelectedFile(lastFile); + setIsNewFile(false); + } + } catch (err) { + console.error('Failed to load last opened file:', err); + setSelectedFile(DEFAULT_FILE.path); + setIsNewFile(true); } }; if (currentWorkspace) { void initializeFile(); + } else { + setSelectedFile(DEFAULT_FILE.path); + setIsNewFile(true); } - }, [currentWorkspace, loadLastOpenedFile, handleFileSelect]); + }, [currentWorkspace, loadLastOpenedFile, saveLastOpenedFile]); return { selectedFile, isNewFile, handleFileSelect }; }; diff --git a/app/src/hooks/useUserAdmin.ts b/app/src/hooks/useUserAdmin.ts index 9258eb0..07e5763 100644 --- a/app/src/hooks/useUserAdmin.ts +++ b/app/src/hooks/useUserAdmin.ts @@ -5,6 +5,7 @@ import { deleteUser as adminDeleteUser, } from '../api/admin'; import { notifications } from '@mantine/notifications'; +import { useCallback } from 'react'; import type { User } from '@/types/models'; import type { CreateUserRequest, UpdateUserRequest } from '@/types/api'; @@ -20,73 +21,77 @@ interface UseUserAdminResult { export const useUserAdmin = (): UseUserAdminResult => { const { data: users, loading, error, reload } = useAdminData('users'); - const handleCreate = async ( - userData: CreateUserRequest - ): Promise => { - try { - await createUser(userData); - notifications.show({ - title: 'Success', - message: 'User created successfully', - color: 'green', - }); - await reload(); - return true; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - notifications.show({ - title: 'Error', - message: `Failed to create user: ${message}`, - color: 'red', - }); - return false; - } - }; + const handleCreate = useCallback( + async (userData: CreateUserRequest): Promise => { + try { + await createUser(userData); + notifications.show({ + title: 'Success', + message: 'User created successfully', + color: 'green', + }); + await reload(); + return true; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + notifications.show({ + title: 'Error', + message: `Failed to create user: ${message}`, + color: 'red', + }); + return false; + } + }, + [reload] + ); - const handleUpdate = async ( - userId: number, - userData: UpdateUserRequest - ): Promise => { - try { - await updateUser(userId, userData); - notifications.show({ - title: 'Success', - message: 'User updated successfully', - color: 'green', - }); - await reload(); - return true; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - notifications.show({ - title: 'Error', - message: `Failed to update user: ${message}`, - color: 'red', - }); - return false; - } - }; + const handleUpdate = useCallback( + async (userId: number, userData: UpdateUserRequest): Promise => { + try { + await updateUser(userId, userData); + notifications.show({ + title: 'Success', + message: 'User updated successfully', + color: 'green', + }); + await reload(); + return true; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + notifications.show({ + title: 'Error', + message: `Failed to update user: ${message}`, + color: 'red', + }); + return false; + } + }, + [reload] + ); - const handleDelete = async (userId: number): Promise => { - try { - await adminDeleteUser(userId); - notifications.show({ - title: 'Success', - message: 'User deleted successfully', - color: 'green', - }); - await reload(); - return true; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - notifications.show({ - title: 'Error', - message: `Failed to delete user: ${message}`, - color: 'red', - }); - return false; - } - }; + const handleDelete = useCallback( + async (userId: number): Promise => { + try { + await adminDeleteUser(userId); + notifications.show({ + title: 'Success', + message: 'User deleted successfully', + color: 'green', + }); + await reload(); + return true; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + notifications.show({ + title: 'Error', + message: `Failed to delete user: ${message}`, + color: 'red', + }); + return false; + } + }, + [reload] + ); return { users, diff --git a/app/src/hooks/useWorkspace.test.ts b/app/src/hooks/useWorkspace.test.ts new file mode 100644 index 0000000..945285f --- /dev/null +++ b/app/src/hooks/useWorkspace.test.ts @@ -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 + ); + }); + }); +});