All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m53s
- Removed direct return of json.data in favor of structured error handling. - Implemented checks for success and data array in useActivityLogQuery, useBestSalePricesQuery, useBrandsQuery, useCategoriesQuery, useFlyerItemsForFlyersQuery, useFlyerItemsQuery, useFlyersQuery, useLeaderboardQuery, useMasterItemsQuery, usePriceHistoryQuery, useShoppingListsQuery, useSuggestedCorrectionsQuery, and useWatchedItemsQuery. - Updated unit tests to reflect changes in expected behavior when API response does not conform to the expected structure. - Updated package.json to use the latest version of @sentry/vite-plugin. - Adjusted vite.config.ts for local development SSL configuration. - Added self-signed SSL certificate and key for local development.
137 lines
4.9 KiB
TypeScript
137 lines
4.9 KiB
TypeScript
// src/hooks/queries/useFlyerItemsQuery.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 { useFlyerItemsQuery } from './useFlyerItemsQuery';
|
|
import * as apiClient from '../../services/apiClient';
|
|
|
|
vi.mock('../../services/apiClient');
|
|
|
|
const mockedApiClient = vi.mocked(apiClient);
|
|
|
|
describe('useFlyerItemsQuery', () => {
|
|
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 when flyerId is provided', async () => {
|
|
const mockFlyerItems = [
|
|
{ item_id: 1, name: 'Milk', price: 3.99, flyer_id: 42 },
|
|
{ item_id: 2, name: 'Bread', price: 2.49, flyer_id: 42 },
|
|
];
|
|
// API returns wrapped response: { success: true, data: [...] }
|
|
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true, data: mockFlyerItems }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsQuery(42), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(mockedApiClient.fetchFlyerItems).toHaveBeenCalledWith(42);
|
|
expect(result.current.data).toEqual(mockFlyerItems);
|
|
});
|
|
|
|
it('should not fetch when flyerId is undefined', async () => {
|
|
const { result } = renderHook(() => useFlyerItemsQuery(undefined), { wrapper });
|
|
|
|
// Wait a bit to ensure the query doesn't run
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
expect(mockedApiClient.fetchFlyerItems).not.toHaveBeenCalled();
|
|
expect(result.current.isLoading).toBe(false);
|
|
expect(result.current.isFetching).toBe(false);
|
|
});
|
|
|
|
it('should handle API error with error message', async () => {
|
|
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
|
ok: false,
|
|
status: 404,
|
|
json: () => Promise.resolve({ message: 'Flyer not found' }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsQuery(999), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
expect(result.current.error?.message).toBe('Flyer not found');
|
|
});
|
|
|
|
it('should handle API error without message', async () => {
|
|
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
json: () => Promise.reject(new Error('Parse error')),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsQuery(42), { 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.fetchFlyerItems.mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
json: () => Promise.resolve({ message: '' }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsQuery(42), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
expect(result.current.error?.message).toBe('Failed to fetch flyer items');
|
|
});
|
|
|
|
// Note: The queryFn contains a guard `if (!flyerId) throw Error('Flyer ID is required')`
|
|
// but this code path is unreachable in normal usage because the query has `enabled: !!flyerId`.
|
|
// When enabled is false, calling refetch() does not execute the queryFn - React Query
|
|
// respects the enabled condition. The guard exists as a defensive measure only.
|
|
|
|
it('should return empty array when API returns no items', async () => {
|
|
// API returns wrapped response: { success: true, data: [] }
|
|
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: true, data: [] }),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsQuery(42), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(result.current.data).toEqual([]);
|
|
});
|
|
|
|
it('should return empty array when response lacks success/data structure (ADR-028)', async () => {
|
|
// ADR-028: API must return { success: true, data: [...] }
|
|
// Non-compliant responses return empty array to prevent .map() errors
|
|
const legacyItems = [{ item_id: 1, name: 'Legacy Item' }];
|
|
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(legacyItems),
|
|
} as Response);
|
|
|
|
const { result } = renderHook(() => useFlyerItemsQuery(42), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
// Returns empty array when response doesn't match ADR-028 format
|
|
expect(result.current.data).toEqual([]);
|
|
});
|
|
});
|