/** * Unit tests for SandboxContext * * Tests the sandbox context provider and hook including: * - Default values when used outside provider * - Providing sandbox status from hooks * - Toggle functionality * - Loading and pending states * - localStorage synchronization */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; // Mock the sandbox hooks vi.mock('../../hooks/useSandbox', () => ({ useSandboxStatus: vi.fn(), useToggleSandbox: vi.fn(), })); import { SandboxProvider, useSandbox } from '../SandboxContext'; import { useSandboxStatus, useToggleSandbox } from '../../hooks/useSandbox'; // Mock localStorage const localStorageMock = (() => { let store: Record = {}; return { getItem: (key: string) => store[key] || null, setItem: (key: string, value: string) => { store[key] = value.toString(); }, removeItem: (key: string) => { delete store[key]; }, clear: () => { store = {}; }, }; })(); Object.defineProperty(window, 'localStorage', { value: localStorageMock, }); // Test wrapper with QueryClient const createWrapper = (queryClient: QueryClient) => { return ({ children }: { children: React.ReactNode }) => ( {children} ); }; describe('SandboxContext', () => { let queryClient: QueryClient; beforeEach(() => { queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 }, mutations: { retry: false }, }, }); vi.clearAllMocks(); localStorageMock.clear(); }); afterEach(() => { queryClient.clear(); localStorageMock.clear(); }); describe('useSandbox hook', () => { it('should return default values when used outside provider', () => { const { result } = renderHook(() => useSandbox()); expect(result.current).toEqual({ isSandbox: false, sandboxEnabled: false, isLoading: false, toggleSandbox: expect.any(Function), isToggling: false, }); }); it('should allow calling toggleSandbox without error when outside provider', async () => { const { result } = renderHook(() => useSandbox()); // Should not throw an error await expect(result.current.toggleSandbox(true)).resolves.toBeUndefined(); }); }); describe('SandboxProvider', () => { describe('sandbox status', () => { it('should provide sandbox status from hook when sandbox is disabled', async () => { const mockStatusData = { sandbox_mode: false, sandbox_enabled: false, }; vi.mocked(useSandboxStatus).mockReturnValue({ data: mockStatusData, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); expect(result.current.isSandbox).toBe(false); expect(result.current.sandboxEnabled).toBe(false); expect(result.current.isLoading).toBe(false); }); it('should provide sandbox status when sandbox is enabled and active', async () => { const mockStatusData = { sandbox_mode: true, sandbox_enabled: true, }; vi.mocked(useSandboxStatus).mockReturnValue({ data: mockStatusData, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); expect(result.current.isSandbox).toBe(true); expect(result.current.sandboxEnabled).toBe(true); expect(result.current.isLoading).toBe(false); }); it('should provide sandbox status when sandbox is enabled but not active', async () => { const mockStatusData = { sandbox_mode: false, sandbox_enabled: true, }; vi.mocked(useSandboxStatus).mockReturnValue({ data: mockStatusData, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); expect(result.current.isSandbox).toBe(false); expect(result.current.sandboxEnabled).toBe(true); expect(result.current.isLoading).toBe(false); }); it('should handle loading state', () => { vi.mocked(useSandboxStatus).mockReturnValue({ data: undefined, isLoading: true, isSuccess: false, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); expect(result.current.isLoading).toBe(true); expect(result.current.isSandbox).toBe(false); expect(result.current.sandboxEnabled).toBe(false); }); it('should default to false when data is undefined', () => { vi.mocked(useSandboxStatus).mockReturnValue({ data: undefined, isLoading: false, isSuccess: false, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); expect(result.current.isSandbox).toBe(false); expect(result.current.sandboxEnabled).toBe(false); }); }); describe('toggleSandbox function', () => { it('should provide toggleSandbox function that calls mutation', async () => { const mockMutateAsync = vi.fn().mockResolvedValue({ sandbox_mode: true }); vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: false, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: mockMutateAsync, isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); await result.current.toggleSandbox(true); expect(mockMutateAsync).toHaveBeenCalledWith(true); expect(mockMutateAsync).toHaveBeenCalledTimes(1); }); it('should call mutation with false to disable sandbox', async () => { const mockMutateAsync = vi.fn().mockResolvedValue({ sandbox_mode: false }); vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: true, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: mockMutateAsync, isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); await result.current.toggleSandbox(false); expect(mockMutateAsync).toHaveBeenCalledWith(false); }); it('should propagate errors from mutation', async () => { const mockError = new Error('Failed to toggle sandbox'); const mockMutateAsync = vi.fn().mockRejectedValue(mockError); vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: false, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: mockMutateAsync, isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); await expect(result.current.toggleSandbox(true)).rejects.toThrow('Failed to toggle sandbox'); }); }); describe('isToggling state', () => { it('should reflect mutation pending state as false', () => { vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: false, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); expect(result.current.isToggling).toBe(false); }); it('should reflect mutation pending state as true', () => { vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: false, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: true, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); expect(result.current.isToggling).toBe(true); }); }); describe('localStorage synchronization', () => { beforeEach(() => { localStorageMock.clear(); }); it('should update localStorage when sandbox_mode is true', async () => { vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: true, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); await waitFor(() => { expect(localStorageMock.getItem('sandbox_mode')).toBe('true'); }); }); it('should update localStorage when sandbox_mode is false', async () => { vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: false, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); await waitFor(() => { expect(localStorageMock.getItem('sandbox_mode')).toBe('false'); }); }); it('should update localStorage when status changes from false to true', async () => { // First render with sandbox_mode = false vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: false, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); const { unmount } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); await waitFor(() => { expect(localStorageMock.getItem('sandbox_mode')).toBe('false'); }); unmount(); // Re-render with sandbox_mode = true vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: true, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); await waitFor(() => { expect(localStorageMock.getItem('sandbox_mode')).toBe('true'); }); }); it('should not update localStorage when sandbox_mode is undefined', async () => { vi.mocked(useSandboxStatus).mockReturnValue({ data: undefined, isLoading: true, isSuccess: false, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); // Wait a bit to ensure effect had time to run await new Promise(resolve => setTimeout(resolve, 100)); expect(localStorageMock.getItem('sandbox_mode')).toBeNull(); }); it('should not update localStorage when status data is partial', async () => { vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_enabled: true } as any, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); // Wait a bit to ensure effect had time to run await new Promise(resolve => setTimeout(resolve, 100)); expect(localStorageMock.getItem('sandbox_mode')).toBeNull(); }); }); describe('integration scenarios', () => { it('should handle complete toggle workflow', async () => { const mockMutateAsync = vi.fn().mockResolvedValue({ sandbox_mode: true }); vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: false, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: mockMutateAsync, isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); // Initial state expect(result.current.isSandbox).toBe(false); expect(result.current.isToggling).toBe(false); expect(localStorageMock.getItem('sandbox_mode')).toBe('false'); // Toggle sandbox await result.current.toggleSandbox(true); expect(mockMutateAsync).toHaveBeenCalledWith(true); }); it('should handle disabled sandbox feature', () => { vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: false, sandbox_enabled: false }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); expect(result.current.isSandbox).toBe(false); expect(result.current.sandboxEnabled).toBe(false); }); it('should handle multiple rapid toggle calls', async () => { const mockMutateAsync = vi.fn() .mockResolvedValueOnce({ sandbox_mode: true }) .mockResolvedValueOnce({ sandbox_mode: false }) .mockResolvedValueOnce({ sandbox_mode: true }); vi.mocked(useSandboxStatus).mockReturnValue({ data: { sandbox_mode: false, sandbox_enabled: true }, isLoading: false, isSuccess: true, isError: false, error: null, } as any); vi.mocked(useToggleSandbox).mockReturnValue({ mutateAsync: mockMutateAsync, isPending: false, } as any); const { result } = renderHook(() => useSandbox(), { wrapper: createWrapper(queryClient), }); // Multiple rapid calls await Promise.all([ result.current.toggleSandbox(true), result.current.toggleSandbox(false), result.current.toggleSandbox(true), ]); expect(mockMutateAsync).toHaveBeenCalledTimes(3); }); }); }); });