xpeditis2.0/apps/frontend/src/__tests__/hooks/useCsvRateSearch.test.tsx
2026-05-12 01:11:04 +02:00

231 lines
6.6 KiB
TypeScript

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();
});
});
});