Files
smoothschedule/frontend/src/contexts/__tests__/SandboxContext.test.tsx
poduck 8dc2248f1f feat: Add comprehensive test suite and misc improvements
- Add frontend unit tests with Vitest for components, hooks, pages, and utilities
- Add backend tests for webhooks, notifications, middleware, and edge cases
- Add ForgotPassword, NotFound, and ResetPassword pages
- Add migration for orphaned staff resources conversion
- Add coverage directory to gitignore (generated reports)
- Various bug fixes and improvements from previous work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 02:36:46 -05:00

582 lines
17 KiB
TypeScript

/**
* 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<string, string> = {};
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 }) => (
<QueryClientProvider client={queryClient}>
<SandboxProvider>{children}</SandboxProvider>
</QueryClientProvider>
);
};
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);
});
});
});
});