Files
flyer-crawler.projectium.com/src/hooks/queries/useFlyerItemsForFlyersQuery.test.tsx
Torben Sorensen c78323275b
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m28s
more unit tests - done for now
2026-01-29 16:21:48 -08:00

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