import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, act, waitFor } from '@testing-library/react'; import React from 'react'; import { WorkspaceDataProvider, useWorkspaceData, } from './WorkspaceDataContext'; import { type Workspace, Theme } from '@/types/models'; // Set up mocks before imports are used vi.mock('@/api/workspace', () => { return { getWorkspace: vi.fn(), listWorkspaces: vi.fn(), getLastWorkspaceName: vi.fn(), updateLastWorkspaceName: vi.fn(), }; }); vi.mock('@mantine/notifications', () => { return { notifications: { show: vi.fn(), }, }; }); vi.mock('./ThemeContext', () => { return { useTheme: vi.fn(), }; }); // Import the mocks after they've been defined import { getWorkspace as mockGetWorkspace, listWorkspaces as mockListWorkspaces, getLastWorkspaceName as mockGetLastWorkspaceName, updateLastWorkspaceName as mockUpdateLastWorkspaceName, } from '@/api/workspace'; import { notifications } from '@mantine/notifications'; import { useTheme } from './ThemeContext'; // Get reference to the mocked functions const mockNotificationsShow = notifications.show as unknown as ReturnType< typeof vi.fn >; const mockUseTheme = useTheme as ReturnType; const mockUpdateColorScheme = vi.fn(); // Mock workspace data const mockWorkspace: Workspace = { id: 1, name: 'test-workspace', theme: Theme.Dark, createdAt: '2024-01-01T00:00:00Z', autoSave: true, showHiddenFiles: false, gitEnabled: false, gitUrl: '', gitUser: '', gitToken: '', gitAutoCommit: false, gitCommitMsgTemplate: '', gitCommitName: '', gitCommitEmail: '', }; const mockWorkspace2: Workspace = { id: 2, name: 'workspace-2', theme: Theme.Light, createdAt: '2024-01-02T00:00:00Z', autoSave: false, showHiddenFiles: false, gitEnabled: false, gitUrl: '', gitUser: '', gitToken: '', gitAutoCommit: false, gitCommitMsgTemplate: '', gitCommitName: '', gitCommitEmail: '', }; const mockWorkspaceList: Workspace[] = [mockWorkspace, mockWorkspace2]; // Helper wrapper component for testing const createWrapper = () => { const Wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); Wrapper.displayName = 'WorkspaceDataProviderTestWrapper'; return Wrapper; }; describe('WorkspaceDataContext', () => { beforeEach(() => { vi.clearAllMocks(); // Setup default theme mock mockUseTheme.mockReturnValue({ colorScheme: 'light', updateColorScheme: mockUpdateColorScheme, }); }); afterEach(() => { vi.restoreAllMocks(); }); describe('WorkspaceDataProvider initialization', () => { it('initializes with null workspace and loading state', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); expect(result.current.currentWorkspace).toBeNull(); expect(result.current.loading).toBe(true); expect(result.current.workspaces).toEqual([]); await waitFor(() => { expect(result.current.loading).toBe(false); }); }); it('provides all expected functions', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); expect(typeof result.current.loadWorkspaces).toBe('function'); expect(typeof result.current.loadWorkspaceData).toBe('function'); expect(typeof result.current.setCurrentWorkspace).toBe('function'); await waitFor(() => { expect(result.current.loading).toBe(false); }); }); it('loads last workspace when available', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( 'test-workspace' ); (mockGetWorkspace as ReturnType).mockResolvedValue( mockWorkspace ); (mockListWorkspaces as ReturnType).mockResolvedValue( mockWorkspaceList ); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.currentWorkspace).toEqual(mockWorkspace); expect(result.current.workspaces).toEqual(mockWorkspaceList); expect(mockGetLastWorkspaceName).toHaveBeenCalledTimes(1); expect(mockGetWorkspace).toHaveBeenCalledWith('test-workspace'); expect(mockListWorkspaces).toHaveBeenCalledTimes(1); expect(mockUpdateColorScheme).toHaveBeenCalledWith('dark'); }); it('loads first available workspace when no last workspace', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue( mockWorkspaceList ); (mockGetWorkspace as ReturnType).mockResolvedValue( mockWorkspace ); ( mockUpdateLastWorkspaceName as ReturnType ).mockResolvedValue(undefined); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.currentWorkspace).toEqual(mockWorkspace); expect(mockUpdateLastWorkspaceName).toHaveBeenCalledWith( 'test-workspace' ); expect(mockGetWorkspace).toHaveBeenCalledWith('test-workspace'); }); it('handles initialization error gracefully', async () => { const consoleSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}); (mockGetLastWorkspaceName as ReturnType).mockRejectedValue( new Error('Network error') ); (mockListWorkspaces as ReturnType).mockResolvedValue( mockWorkspaceList ); (mockGetWorkspace as ReturnType).mockResolvedValue( mockWorkspace ); ( mockUpdateLastWorkspaceName as ReturnType ).mockResolvedValue(undefined); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(consoleSpy).toHaveBeenCalledWith( 'Failed to initialize workspace:', expect.any(Error) ); // Should fallback to loading first available workspace expect(result.current.currentWorkspace).toEqual(mockWorkspace); consoleSpy.mockRestore(); }); it('handles case when no workspaces are available', async () => { const consoleSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}); (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.currentWorkspace).toBeNull(); expect(result.current.workspaces).toEqual([]); consoleSpy.mockRestore(); }); }); describe('useWorkspaceData hook', () => { it('throws error when used outside WorkspaceDataProvider', () => { const consoleSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}); expect(() => { renderHook(() => useWorkspaceData()); }).toThrow( 'useWorkspaceData must be used within a WorkspaceDataProvider' ); consoleSpy.mockRestore(); }); it('returns workspace context when used within provider', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current).toBeDefined(); expect(typeof result.current).toBe('object'); }); it('maintains function stability across re-renders', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); const wrapper = createWrapper(); const { result, rerender } = renderHook(() => useWorkspaceData(), { wrapper, }); await waitFor(() => { expect(result.current.loading).toBe(false); }); const initialFunctions = { loadWorkspaces: result.current.loadWorkspaces, loadWorkspaceData: result.current.loadWorkspaceData, setCurrentWorkspace: result.current.setCurrentWorkspace, }; rerender(); expect(result.current.loadWorkspaces).toBe( initialFunctions.loadWorkspaces ); expect(result.current.loadWorkspaceData).toBe( initialFunctions.loadWorkspaceData ); expect(result.current.setCurrentWorkspace).toBe( initialFunctions.setCurrentWorkspace ); }); }); describe('loadWorkspaces functionality', () => { beforeEach(() => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); }); it('loads workspaces successfully', async () => { (mockListWorkspaces as ReturnType).mockResolvedValue( mockWorkspaceList ); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); let workspaces: Workspace[] | undefined; await act(async () => { workspaces = await result.current.loadWorkspaces(); }); expect(workspaces).toEqual(mockWorkspaceList); expect(result.current.workspaces).toEqual(mockWorkspaceList); }); it('handles loadWorkspaces failure', async () => { const consoleSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}); (mockListWorkspaces as ReturnType) .mockResolvedValueOnce([]) // Initial load .mockRejectedValueOnce(new Error('Failed to load workspaces')); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); let workspaces: Workspace[] | undefined; await act(async () => { workspaces = await result.current.loadWorkspaces(); }); expect(workspaces).toEqual([]); expect(consoleSpy).toHaveBeenCalledWith( 'Failed to load workspaces:', expect.any(Error) ); expect(mockNotificationsShow).toHaveBeenCalledWith({ title: 'Error', message: 'Failed to load workspaces list', color: 'red', }); consoleSpy.mockRestore(); }); }); describe('loadWorkspaceData functionality', () => { beforeEach(() => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); }); it('loads workspace data successfully', async () => { (mockGetWorkspace as ReturnType).mockResolvedValue( mockWorkspace ); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); await act(async () => { await result.current.loadWorkspaceData('test-workspace'); }); expect(result.current.currentWorkspace).toEqual(mockWorkspace); expect(mockGetWorkspace).toHaveBeenCalledWith('test-workspace'); expect(mockUpdateColorScheme).toHaveBeenCalledWith('dark'); }); it('handles loadWorkspaceData failure', async () => { const consoleSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}); (mockGetWorkspace as ReturnType).mockRejectedValue( new Error('Workspace not found') ); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); await act(async () => { await result.current.loadWorkspaceData('nonexistent-workspace'); }); expect(result.current.currentWorkspace).toBeNull(); expect(consoleSpy).toHaveBeenCalledWith( 'Failed to load workspace data:', expect.any(Error) ); expect(mockNotificationsShow).toHaveBeenCalledWith({ title: 'Error', message: 'Failed to load workspace data', color: 'red', }); consoleSpy.mockRestore(); }); it('updates theme when loading workspace', async () => { const lightThemeWorkspace = { ...mockWorkspace, theme: 'light' }; (mockGetWorkspace as ReturnType).mockResolvedValue( lightThemeWorkspace ); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); await act(async () => { await result.current.loadWorkspaceData('test-workspace'); }); expect(mockUpdateColorScheme).toHaveBeenCalledWith('light'); }); }); describe('setCurrentWorkspace functionality', () => { beforeEach(() => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); }); it('sets current workspace', async () => { const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); act(() => { result.current.setCurrentWorkspace(mockWorkspace); }); expect(result.current.currentWorkspace).toEqual(mockWorkspace); }); it('sets workspace to null', async () => { const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); // Set a workspace first act(() => { result.current.setCurrentWorkspace(mockWorkspace); }); expect(result.current.currentWorkspace).toEqual(mockWorkspace); // Then set it to null act(() => { result.current.setCurrentWorkspace(null); }); expect(result.current.currentWorkspace).toBeNull(); }); }); describe('workspace state transitions', () => { beforeEach(() => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); }); it('transitions from null to workspace', async () => { (mockGetWorkspace as ReturnType).mockResolvedValue( mockWorkspace ); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.currentWorkspace).toBeNull(); await act(async () => { await result.current.loadWorkspaceData('test-workspace'); }); expect(result.current.currentWorkspace).toEqual(mockWorkspace); }); it('transitions between different workspaces', async () => { (mockGetWorkspace as ReturnType) .mockResolvedValueOnce(mockWorkspace) .mockResolvedValueOnce(mockWorkspace2); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); // Load first workspace await act(async () => { await result.current.loadWorkspaceData('test-workspace'); }); expect(result.current.currentWorkspace).toEqual(mockWorkspace); expect(mockUpdateColorScheme).toHaveBeenCalledWith('dark'); // Load second workspace await act(async () => { await result.current.loadWorkspaceData('workspace-2'); }); expect(result.current.currentWorkspace).toEqual(mockWorkspace2); expect(mockUpdateColorScheme).toHaveBeenCalledWith('light'); }); }); describe('context value structure', () => { it('provides expected context interface when no workspace loaded', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.currentWorkspace).toBeNull(); expect(result.current.workspaces).toEqual([]); expect(result.current.loading).toBe(false); expect(typeof result.current.loadWorkspaces).toBe('function'); expect(typeof result.current.loadWorkspaceData).toBe('function'); expect(typeof result.current.setCurrentWorkspace).toBe('function'); }); it('provides correct context when workspace loaded', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( 'test-workspace' ); (mockGetWorkspace as ReturnType).mockResolvedValue( mockWorkspace ); (mockListWorkspaces as ReturnType).mockResolvedValue( mockWorkspaceList ); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.currentWorkspace).toEqual(mockWorkspace); expect(result.current.workspaces).toEqual(mockWorkspaceList); expect(result.current.loading).toBe(false); expect(typeof result.current.loadWorkspaces).toBe('function'); expect(typeof result.current.loadWorkspaceData).toBe('function'); expect(typeof result.current.setCurrentWorkspace).toBe('function'); }); }); describe('loading states', () => { it('shows loading during initialization', async () => { let resolveGetLastWorkspaceName: (value: string | null) => void; const pendingPromise = new Promise((resolve) => { resolveGetLastWorkspaceName = resolve; }); (mockGetLastWorkspaceName as ReturnType).mockReturnValue( pendingPromise ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); expect(result.current.loading).toBe(true); await act(async () => { resolveGetLastWorkspaceName!(null); await pendingPromise; }); await waitFor(() => { expect(result.current.loading).toBe(false); }); }); it('clears loading after initialization completes', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( 'test-workspace' ); (mockGetWorkspace as ReturnType).mockResolvedValue( mockWorkspace ); (mockListWorkspaces as ReturnType).mockResolvedValue( mockWorkspaceList ); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); }); it('clears loading after initialization fails', async () => { (mockGetLastWorkspaceName as ReturnType).mockRejectedValue( new Error('Init failed') ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); }); }); describe('concurrent operations', () => { it('handles concurrent loadWorkspaceData calls', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType).mockResolvedValue([]); (mockGetWorkspace as ReturnType) .mockResolvedValueOnce(mockWorkspace) .mockResolvedValueOnce(mockWorkspace2); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); // Make concurrent calls await act(async () => { await Promise.all([ result.current.loadWorkspaceData('test-workspace'), result.current.loadWorkspaceData('workspace-2'), ]); }); expect(mockGetWorkspace).toHaveBeenCalledTimes(2); expect(mockGetWorkspace).toHaveBeenCalledWith('test-workspace'); expect(mockGetWorkspace).toHaveBeenCalledWith('workspace-2'); }); it('handles concurrent loadWorkspaces calls', async () => { (mockGetLastWorkspaceName as ReturnType).mockResolvedValue( null ); (mockListWorkspaces as ReturnType) .mockResolvedValueOnce([]) // Initial load .mockResolvedValue(mockWorkspaceList) // Subsequent calls .mockResolvedValue(mockWorkspaceList); const wrapper = createWrapper(); const { result } = renderHook(() => useWorkspaceData(), { wrapper }); await waitFor(() => { expect(result.current.loading).toBe(false); }); // Make concurrent calls const [result1, result2] = await act(async () => { return Promise.all([ result.current.loadWorkspaces(), result.current.loadWorkspaces(), ]); }); expect(result1).toEqual(mockWorkspaceList); expect(result2).toEqual(mockWorkspaceList); expect(result.current.workspaces).toEqual(mockWorkspaceList); }); }); });