Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m28s
311 lines
10 KiB
TypeScript
311 lines
10 KiB
TypeScript
// src/hooks/queries/useFlyerItemsForFlyersQuery.test.tsx
|
|
import { renderHook, waitFor } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import type { ReactNode } from 'react';
|
|
import { useFlyerItemsForFlyersQuery } from './useFlyerItemsForFlyersQuery';
|
|
import * as apiClient from '../../services/apiClient';
|
|
import type { FlyerItem } from '../../types';
|
|
import { createMockFlyerItem } from '../../tests/utils/mockFactories';
|
|
|
|
vi.mock('../../services/apiClient');
|
|
|
|
const mockedApiClient = vi.mocked(apiClient);
|
|
|
|
describe('useFlyerItemsForFlyersQuery', () => {
|
|
let queryClient: QueryClient;
|
|
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should fetch flyer items for multiple flyers successfully', async () => {
|
|
const mockFlyerItems: FlyerItem[] = [
|
|
createMockFlyerItem({
|
|
flyer_item_id: 1,
|
|
flyer_id: 100,
|
|
item: 'Organic Bananas',
|
|
price_display: '$0.59/lb',
|
|
price_in_cents: 59,
|
|
quantity: 'lb',
|
|
master_item_id: 1001,
|
|
master_item_name: 'Bananas',
|
|
category_id: 1,
|
|
category_name: 'Produce',
|
|
}),
|
|
createMockFlyerItem({
|
|
flyer_item_id: 2,
|
|
flyer_id: 100,
|
|
item: 'Whole Milk',
|
|
price_display: '$3.99',
|
|
price_in_cents: 399,
|
|
quantity: 'gal',
|
|
master_item_id: 1002,
|
|
master_item_name: 'Milk',
|
|
category_id: 2,
|
|
category_name: 'Dairy',
|
|
}),
|
|
createMockFlyerItem({
|
|
flyer_item_id: 3,
|
|
flyer_id: 101,
|
|
item: 'Chicken Breast',
|
|
price_display: '$5.99/lb',
|
|
price_in_cents: 599,
|
|
quantity: 'lb',
|
|
master_item_id: 1003,
|
|
master_item_name: 'Chicken',
|
|
category_id: 3,
|
|
category_name: 'Meat',
|
|
}),
|
|
];
|
|
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true, data: mockFlyerItems }),
|
|
} as Response);
|
|
|
|
const flyerIds = [100, 101];
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery(flyerIds), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledWith(flyerIds);
|
|
expect(result.current.data).toEqual(mockFlyerItems);
|
|
expect(result.current.data).toHaveLength(3);
|
|
});
|
|
|
|
it('should handle API error with error message', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: false,
|
|
status: 401,
|
|
json: () => Promise.resolve({ message: 'Authentication required' }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([100]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
expect(result.current.error?.message).toBe('Authentication required');
|
|
});
|
|
|
|
it('should handle API error without message (JSON parse error)', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
json: () => Promise.reject(new Error('Parse error')),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([100]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
expect(result.current.error?.message).toBe('Request failed with status 500');
|
|
});
|
|
|
|
it('should use fallback message when error.message is empty', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
json: () => Promise.resolve({ message: '' }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([100]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
expect(result.current.error?.message).toBe('Failed to fetch flyer items');
|
|
});
|
|
|
|
it('should return empty array for no flyer items', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true, data: [] }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([100]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(result.current.data).toEqual([]);
|
|
});
|
|
|
|
it('should not fetch when disabled explicitly', () => {
|
|
renderHook(() => useFlyerItemsForFlyersQuery([100], false), { wrapper });
|
|
|
|
expect(mockedApiClient.fetchFlyerItemsForFlyers).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not fetch when flyerIds array is empty', () => {
|
|
renderHook(() => useFlyerItemsForFlyersQuery([]), { wrapper });
|
|
|
|
expect(mockedApiClient.fetchFlyerItemsForFlyers).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not fetch when flyerIds is empty even if enabled is true', () => {
|
|
renderHook(() => useFlyerItemsForFlyersQuery([], true), { wrapper });
|
|
|
|
expect(mockedApiClient.fetchFlyerItemsForFlyers).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return empty array when success is false in response', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: false, error: 'Some error' }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([100]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(result.current.data).toEqual([]);
|
|
});
|
|
|
|
it('should return empty array when data is not an array', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true, data: null }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([100]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(result.current.data).toEqual([]);
|
|
});
|
|
|
|
it('should return empty array when data is an object instead of array', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true, data: { item: 'not an array' } }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([100]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(result.current.data).toEqual([]);
|
|
});
|
|
|
|
it('should fetch for single flyer ID', async () => {
|
|
const mockFlyerItems: FlyerItem[] = [
|
|
createMockFlyerItem({
|
|
flyer_item_id: 1,
|
|
flyer_id: 100,
|
|
item: 'Bread',
|
|
price_display: '$2.49',
|
|
price_in_cents: 249,
|
|
}),
|
|
];
|
|
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true, data: mockFlyerItems }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([100]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledWith([100]);
|
|
expect(result.current.data).toEqual(mockFlyerItems);
|
|
});
|
|
|
|
it('should handle 404 error status', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: false,
|
|
status: 404,
|
|
json: () => Promise.resolve({ message: 'Flyers not found' }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([999]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
expect(result.current.error?.message).toBe('Flyers not found');
|
|
});
|
|
|
|
it('should handle network error', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockRejectedValue(new Error('Network error'));
|
|
|
|
const { result } = renderHook(() => useFlyerItemsForFlyersQuery([100]), {
|
|
wrapper,
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
expect(result.current.error?.message).toBe('Network error');
|
|
});
|
|
|
|
it('should be enabled by default when flyerIds has items', async () => {
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true, data: [] }),
|
|
} as Response);
|
|
|
|
// Call without the enabled parameter (uses default value of true)
|
|
renderHook(() => useFlyerItemsForFlyersQuery([100]), { wrapper });
|
|
|
|
await waitFor(() => expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalled());
|
|
});
|
|
|
|
it('should use consistent query key regardless of flyer IDs order', async () => {
|
|
const mockItems: FlyerItem[] = [createMockFlyerItem({ flyer_item_id: 1, flyer_id: 100 })];
|
|
|
|
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true, data: mockItems }),
|
|
} as Response);
|
|
|
|
// First call with [100, 200, 50]
|
|
const { result: result1 } = renderHook(() => useFlyerItemsForFlyersQuery([100, 200, 50]), {
|
|
wrapper,
|
|
});
|
|
await waitFor(() => expect(result1.current.isSuccess).toBe(true));
|
|
|
|
// API should be called with original order
|
|
expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledWith([100, 200, 50]);
|
|
|
|
// Second call with same IDs in different order should use cached result
|
|
// because query key uses sorted IDs (50,100,200)
|
|
const { result: result2 } = renderHook(() => useFlyerItemsForFlyersQuery([50, 200, 100]), {
|
|
wrapper,
|
|
});
|
|
|
|
// Should immediately have data from cache (no additional API call)
|
|
await waitFor(() => expect(result2.current.isSuccess).toBe(true));
|
|
|
|
// API should still only have been called once (cached)
|
|
expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledTimes(1);
|
|
expect(result2.current.data).toEqual(mockItems);
|
|
});
|
|
});
|