import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; vi.mock('../../api/client', () => ({ default: { get: vi.fn(), }, })); import { useResourceLocation, useLiveResourceLocation } from '../useResourceLocation'; import apiClient from '../../api/client'; const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return function Wrapper({ children }: { children: React.ReactNode }) { return React.createElement(QueryClientProvider, { client: queryClient }, children); }; }; describe('useResourceLocation', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('data transformation', () => { it('should transform snake_case to camelCase for basic location data', async () => { const mockResponse = { data: { has_location: true, latitude: 40.7128, longitude: -74.0060, accuracy: 10, heading: 180, speed: 5.5, timestamp: '2025-12-07T12:00:00Z', is_tracking: true, message: 'Location updated', }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const { result } = renderHook(() => useResourceLocation('123'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data).toEqual({ hasLocation: true, latitude: 40.7128, longitude: -74.0060, accuracy: 10, heading: 180, speed: 5.5, timestamp: '2025-12-07T12:00:00Z', isTracking: true, activeJob: null, message: 'Location updated', }); }); it('should transform activeJob with status_display to statusDisplay', async () => { const mockResponse = { data: { has_location: true, latitude: 40.7128, longitude: -74.0060, is_tracking: true, active_job: { id: 456, title: 'Repair HVAC System', status: 'en_route', status_display: 'En Route', }, }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const { result } = renderHook(() => useResourceLocation('123'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.activeJob).toEqual({ id: 456, title: 'Repair HVAC System', status: 'en_route', statusDisplay: 'En Route', }); }); it('should set activeJob to null when not provided', async () => { const mockResponse = { data: { has_location: false, is_tracking: false, }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const { result } = renderHook(() => useResourceLocation('123'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.activeJob).toBeNull(); }); it('should default isTracking to false when not provided', async () => { const mockResponse = { data: { has_location: true, latitude: 40.7128, longitude: -74.0060, // is_tracking not provided }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const { result } = renderHook(() => useResourceLocation('123'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.isTracking).toBe(false); }); it('should handle null active_job explicitly', async () => { const mockResponse = { data: { has_location: true, latitude: 40.7128, longitude: -74.0060, is_tracking: true, active_job: null, }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const { result } = renderHook(() => useResourceLocation('123'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.activeJob).toBeNull(); }); }); describe('API calls', () => { it('should call the correct API endpoint with resourceId', async () => { const mockResponse = { data: { has_location: false, is_tracking: false, }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const { result } = renderHook(() => useResourceLocation('789'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(apiClient.get).toHaveBeenCalledWith('/resources/789/location/'); expect(apiClient.get).toHaveBeenCalledTimes(1); }); it('should not fetch when resourceId is null', () => { const { result } = renderHook(() => useResourceLocation(null), { wrapper: createWrapper(), }); expect(result.current.isPending).toBe(true); expect(result.current.fetchStatus).toBe('idle'); expect(apiClient.get).not.toHaveBeenCalled(); }); it('should not fetch when enabled is false', () => { const { result } = renderHook( () => useResourceLocation('123', { enabled: false }), { wrapper: createWrapper(), } ); expect(result.current.isPending).toBe(true); expect(result.current.fetchStatus).toBe('idle'); expect(apiClient.get).not.toHaveBeenCalled(); }); it('should fetch when enabled is true', async () => { const mockResponse = { data: { has_location: true, is_tracking: true, }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const { result } = renderHook( () => useResourceLocation('123', { enabled: true }), { wrapper: createWrapper(), } ); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(apiClient.get).toHaveBeenCalledWith('/resources/123/location/'); }); }); describe('error handling', () => { it('should handle API errors', async () => { const mockError = new Error('Network error'); vi.mocked(apiClient.get).mockRejectedValueOnce(mockError); const { result } = renderHook(() => useResourceLocation('123'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isError).toBe(true)); expect(result.current.error).toEqual(mockError); expect(result.current.data).toBeUndefined(); }); it('should handle 404 responses', async () => { const mockError = { response: { status: 404, data: { detail: 'Resource not found' }, }, }; vi.mocked(apiClient.get).mockRejectedValueOnce(mockError); const { result } = renderHook(() => useResourceLocation('999'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isError).toBe(true)); expect(result.current.error).toEqual(mockError); }); }); describe('query configuration', () => { it('should use the correct query key', async () => { const mockResponse = { data: { has_location: false, is_tracking: false, }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useResourceLocation('123'), { wrapper }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); const cachedData = queryClient.getQueryData(['resourceLocation', '123']); expect(cachedData).toBeDefined(); expect(cachedData).toEqual(result.current.data); }); it('should not refetch automatically', async () => { const mockResponse = { data: { has_location: true, is_tracking: true, }, }; vi.mocked(apiClient.get).mockResolvedValue(mockResponse); const { result } = renderHook(() => useResourceLocation('123'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); // Wait a bit to ensure no automatic refetch await new Promise(resolve => setTimeout(resolve, 100)); // Should only be called once (no refetchInterval) expect(apiClient.get).toHaveBeenCalledTimes(1); }); }); describe('optional fields', () => { it('should handle missing optional location fields', async () => { const mockResponse = { data: { has_location: true, latitude: 40.7128, longitude: -74.0060, is_tracking: true, // accuracy, heading, speed, timestamp not provided }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const { result } = renderHook(() => useResourceLocation('123'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data).toEqual({ hasLocation: true, latitude: 40.7128, longitude: -74.0060, accuracy: undefined, heading: undefined, speed: undefined, timestamp: undefined, isTracking: true, activeJob: null, message: undefined, }); }); it('should handle message field when provided', async () => { const mockResponse = { data: { has_location: false, is_tracking: false, message: 'Resource has not started tracking yet', }, }; vi.mocked(apiClient.get).mockResolvedValueOnce(mockResponse); const { result } = renderHook(() => useResourceLocation('123'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.message).toBe('Resource has not started tracking yet'); }); }); }); describe('useLiveResourceLocation', () => { let mockWebSocket: { close: ReturnType; send: ReturnType; addEventListener: ReturnType; removeEventListener: ReturnType; readyState: number; onopen: ((event: Event) => void) | null; onmessage: ((event: MessageEvent) => void) | null; onerror: ((event: Event) => void) | null; onclose: ((event: CloseEvent) => void) | null; }; beforeEach(() => { vi.clearAllMocks(); // Mock WebSocket mockWebSocket = { close: vi.fn(), send: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), readyState: 1, // OPEN onopen: null, onmessage: null, onerror: null, onclose: null, }; // Mock WebSocket constructor properly global.WebSocket = vi.fn(function(this: any) { return mockWebSocket; }) as any; }); afterEach(() => { vi.restoreAllMocks(); }); it('should not crash when rendered', () => { const { result } = renderHook(() => useLiveResourceLocation('123'), { wrapper: createWrapper(), }); expect(result.current).toBeDefined(); expect(result.current.refresh).toBeInstanceOf(Function); }); it('should create WebSocket connection with correct URL', () => { renderHook(() => useLiveResourceLocation('456'), { wrapper: createWrapper(), }); expect(global.WebSocket).toHaveBeenCalledWith( expect.stringContaining('/ws/resource-location/456/') ); }); it('should not connect when resourceId is null', () => { renderHook(() => useLiveResourceLocation(null), { wrapper: createWrapper(), }); expect(global.WebSocket).not.toHaveBeenCalled(); }); it('should not connect when enabled is false', () => { renderHook(() => useLiveResourceLocation('123', { enabled: false }), { wrapper: createWrapper(), }); expect(global.WebSocket).not.toHaveBeenCalled(); }); it('should close WebSocket on unmount', () => { const { unmount } = renderHook(() => useLiveResourceLocation('123'), { wrapper: createWrapper(), }); unmount(); expect(mockWebSocket.close).toHaveBeenCalledWith(1000, 'Component unmounting'); }); it('should return refresh function', () => { const { result } = renderHook(() => useLiveResourceLocation('123'), { wrapper: createWrapper(), }); expect(result.current.refresh).toBeInstanceOf(Function); // Should not throw when called expect(() => result.current.refresh()).not.toThrow(); }); it('should handle location_update message type', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useLiveResourceLocation('123'), { wrapper }); // Simulate WebSocket message const mockMessage = { data: JSON.stringify({ type: 'location_update', latitude: 40.7128, longitude: -74.0060, accuracy: 10, heading: 180, speed: 5.5, timestamp: '2025-12-07T12:00:00Z', }), }; if (mockWebSocket.onmessage) { mockWebSocket.onmessage(mockMessage as MessageEvent); } // Verify query cache was updated const cachedData = queryClient.getQueryData(['resourceLocation', '123']); expect(cachedData).toBeDefined(); }); it('should handle tracking_stopped message type', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); // Set initial data queryClient.setQueryData(['resourceLocation', '123'], { hasLocation: true, isTracking: true, latitude: 40.7128, longitude: -74.0060, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useLiveResourceLocation('123'), { wrapper }); // Simulate tracking stopped message const mockMessage = { data: JSON.stringify({ type: 'tracking_stopped', }), }; if (mockWebSocket.onmessage) { mockWebSocket.onmessage(mockMessage as MessageEvent); } // Verify isTracking was set to false const cachedData = queryClient.getQueryData(['resourceLocation', '123']); expect(cachedData?.isTracking).toBe(false); }); it('should handle malformed WebSocket messages gracefully', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); renderHook(() => useLiveResourceLocation('123'), { wrapper: createWrapper(), }); // Simulate malformed JSON const mockMessage = { data: 'invalid json{{{', }; if (mockWebSocket.onmessage) { mockWebSocket.onmessage(mockMessage as MessageEvent); } // Should log error but not crash expect(consoleErrorSpy).toHaveBeenCalled(); consoleErrorSpy.mockRestore(); }); });