import { renderHook, act } from '@testing-library/react'; import { useCsvRateSearch } from '@/hooks/useCsvRateSearch'; import { searchCsvRatesWithOffers } from '@/lib/api/rates'; import type { CsvRateSearchRequest, CsvRateSearchResponse } from '@/types/rates'; jest.mock('@/lib/api/rates', () => ({ searchCsvRatesWithOffers: jest.fn(), })); const mockSearchCsvRatesWithOffers = jest.mocked(searchCsvRatesWithOffers); const mockRequest: CsvRateSearchRequest = { origin: 'FRLEH', destination: 'CNSHA', volumeCBM: 10, weightKG: 5000, }; const mockResponse: CsvRateSearchResponse = { results: [ { companyName: 'SSC Consolidation', companyEmail: 'bookings@ssc.com', originCFS: 'Le Havre', origin: 'FRLEH', portOfLoading: 'LE HAVRE', routing: 'Direct', destinationCFS: 'Shanghai', destination: 'CNSHA', destinationCountry: 'China', containerType: 'LCL', priceBreakdown: { freightCharge: 440, freightCurrency: 'USD', fobFixed: 173, fobHandling: 110, fobDG: 0, fobCurrency: 'EUR', fobBreakdown: { documentation: 55, isps: 18, handling: 110, solas: 15, customs: 85, ams_aci: 0, isf5: 0, dgAdmin: 0, }, dgSurchargeAmount: null, dgSurchargeCurrency: 'EUR', dgSurchargeStatus: 'computed', totalFreight: 440, totalFob: 283, totalPriceForSorting: 723, primaryCurrency: 'USD', }, frequency: 'Weekly', transitDays: 33, validUntil: '2026-12-31', dgAccepted: true, dgSurchargeStatus: 'computed', remarks: '', source: 'CSV', matchScore: 110, serviceLevel: 'STANDARD', priceMultiplier: 1.0, originalTransitDays: 33, adjustedTransitDays: 33, }, ], totalResults: 1, searchedFiles: ['ssc-consolidation.csv'], searchedAt: '2026-05-11T10:00:00Z', appliedFilters: {}, }; beforeEach(() => { jest.clearAllMocks(); }); describe('useCsvRateSearch', () => { describe('initial state', () => { it('starts with data=null', () => { const { result } = renderHook(() => useCsvRateSearch()); expect(result.current.data).toBeNull(); }); it('starts with loading=false', () => { const { result } = renderHook(() => useCsvRateSearch()); expect(result.current.loading).toBe(false); }); it('starts with error=null', () => { const { result } = renderHook(() => useCsvRateSearch()); expect(result.current.error).toBeNull(); }); it('exposes a search function', () => { const { result } = renderHook(() => useCsvRateSearch()); expect(typeof result.current.search).toBe('function'); }); it('exposes a reset function', () => { const { result } = renderHook(() => useCsvRateSearch()); expect(typeof result.current.reset).toBe('function'); }); }); describe('search — success path', () => { it('sets loading=true while the request is in flight', async () => { let resolveSearch: (v: any) => void; mockSearchCsvRatesWithOffers.mockReturnValue( new Promise(resolve => { resolveSearch = resolve; }) ); const { result } = renderHook(() => useCsvRateSearch()); act(() => { result.current.search(mockRequest); }); expect(result.current.loading).toBe(true); await act(async () => { resolveSearch!(mockResponse); }); }); it('sets data and clears loading after a successful search', async () => { mockSearchCsvRatesWithOffers.mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useCsvRateSearch()); await act(async () => { await result.current.search(mockRequest); }); expect(result.current.loading).toBe(false); expect(result.current.data).toEqual(mockResponse); expect(result.current.error).toBeNull(); }); it('calls searchCsvRatesWithOffers with the given request', async () => { mockSearchCsvRatesWithOffers.mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useCsvRateSearch()); await act(async () => { await result.current.search(mockRequest); }); expect(mockSearchCsvRatesWithOffers).toHaveBeenCalledWith(mockRequest); }); it('clears a previous error when a new search starts', async () => { mockSearchCsvRatesWithOffers.mockRejectedValueOnce(new Error('first error')); mockSearchCsvRatesWithOffers.mockResolvedValueOnce(mockResponse as any); const { result } = renderHook(() => useCsvRateSearch()); await act(async () => { await result.current.search(mockRequest); }); expect(result.current.error).toBe('first error'); await act(async () => { await result.current.search(mockRequest); }); expect(result.current.error).toBeNull(); }); }); describe('search — error path', () => { it('sets error and clears data when the API throws', async () => { mockSearchCsvRatesWithOffers.mockRejectedValue(new Error('Network error')); const { result } = renderHook(() => useCsvRateSearch()); await act(async () => { await result.current.search(mockRequest); }); expect(result.current.error).toBe('Network error'); expect(result.current.data).toBeNull(); expect(result.current.loading).toBe(false); }); it('uses a default error message when the error has no message', async () => { mockSearchCsvRatesWithOffers.mockRejectedValue({}); const { result } = renderHook(() => useCsvRateSearch()); await act(async () => { await result.current.search(mockRequest); }); expect(result.current.error).toBe('Erreur lors de la recherche de tarifs'); }); }); describe('reset', () => { it('clears data, error, and loading', async () => { mockSearchCsvRatesWithOffers.mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useCsvRateSearch()); await act(async () => { await result.current.search(mockRequest); }); act(() => { result.current.reset(); }); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); expect(result.current.loading).toBe(false); }); it('can be called before any search without throwing', () => { const { result } = renderHook(() => useCsvRateSearch()); expect(() => { act(() => result.current.reset()); }).not.toThrow(); }); }); });