231 lines
6.6 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|