Files
flyer-crawler.projectium.com/src/hooks/useUserData.test.tsx
Torben Sorensen e457bbf046
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 26m51s
more req work
2026-01-09 00:18:09 -08:00

252 lines
8.2 KiB
TypeScript

// src/hooks/useUserData.test.tsx
import React, { ReactNode } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useUserData } from './useUserData';
import { useAuth } from './useAuth';
import { UserDataProvider } from '../providers/UserDataProvider';
import { useWatchedItemsQuery } from './queries/useWatchedItemsQuery';
import { useShoppingListsQuery } from './queries/useShoppingListsQuery';
import type { UserProfile } from '../types';
import {
createMockMasterGroceryItem,
createMockShoppingList,
createMockUserProfile,
} from '../tests/utils/mockFactories';
// 1. Mock the hook's dependencies
vi.mock('../hooks/useAuth');
vi.mock('./queries/useWatchedItemsQuery');
vi.mock('./queries/useShoppingListsQuery');
// 2. Create typed mocks for type safety and autocompletion
const mockedUseAuth = vi.mocked(useAuth);
const mockedUseWatchedItemsQuery = vi.mocked(useWatchedItemsQuery);
const mockedUseShoppingListsQuery = vi.mocked(useShoppingListsQuery);
// 3. A simple wrapper component that renders our provider.
// This is necessary because the useUserData hook needs to be a child of UserDataProvider.
const wrapper = ({ children }: { children: ReactNode }) => (
<UserDataProvider>{children}</UserDataProvider>
); // No change needed here, just for context
// 4. Mock data for testing
const mockUser: UserProfile = createMockUserProfile({
full_name: 'Test User',
points: 100,
user: { user_id: 'user-123', email: 'test@example.com' },
});
const mockWatchedItems = [createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' })];
const mockShoppingLists = [
createMockShoppingList({ shopping_list_id: 1, name: 'Weekly Groceries', user_id: 'user-123' }),
];
describe('useUserData Hook and UserDataProvider', () => {
beforeEach(() => {
// Clear mock history before each test to ensure test isolation.
vi.clearAllMocks();
});
it('should throw an error if useUserData is used outside of UserDataProvider', () => {
// Suppress the expected console.error for this test to keep the output clean.
const originalError = console.error;
console.error = vi.fn();
// Expecting renderHook to throw an error because there's no provider.
expect(() => renderHook(() => useUserData())).toThrow(
'useUserData must be used within a UserDataProvider',
);
// Restore the original console.error function.
console.error = originalError;
});
it('should return initial empty state when user is not authenticated', () => {
// Arrange: Simulate a logged-out user.
mockedUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
// Arrange: Mock the return value of the inner hooks.
mockedUseWatchedItemsQuery.mockReturnValue({
data: [],
isLoading: false,
error: null,
} as any);
mockedUseShoppingListsQuery.mockReturnValue({
data: [],
isLoading: false,
error: null,
} as any);
// Act: Render the hook within the provider wrapper.
const { result } = renderHook(() => useUserData(), { wrapper });
// Assert: Check that the context values are in their initial, empty state.
expect(result.current.isLoading).toBe(false);
expect(result.current.watchedItems).toEqual([]);
expect(result.current.shoppingLists).toEqual([]);
expect(result.current.error).toBeNull();
// Assert: Check that queries were disabled (called with false)
expect(mockedUseWatchedItemsQuery).toHaveBeenCalledWith(false);
expect(mockedUseShoppingListsQuery).toHaveBeenCalledWith(false);
});
it('should return loading state when user is authenticated and data is fetching', () => {
// Arrange: Simulate a logged-in user.
mockedUseAuth.mockReturnValue({
userProfile: mockUser,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
// Arrange: Mock one of the inner hooks to be in a loading state.
mockedUseWatchedItemsQuery.mockReturnValue({
data: undefined,
isLoading: true,
error: null,
} as any);
mockedUseShoppingListsQuery.mockReturnValue({
data: undefined,
isLoading: false,
error: null,
} as any);
// Act
const { result } = renderHook(() => useUserData(), { wrapper });
// Assert
expect(result.current.isLoading).toBe(true);
});
it('should return data on successful fetch when user is authenticated', async () => {
// Arrange: Simulate a logged-in user.
mockedUseAuth.mockReturnValue({
userProfile: mockUser,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
// Arrange: Mock successful data fetches for both inner hooks.
mockedUseWatchedItemsQuery.mockReturnValue({
data: mockWatchedItems,
isLoading: false,
error: null,
} as any);
mockedUseShoppingListsQuery.mockReturnValue({
data: mockShoppingLists,
isLoading: false,
error: null,
} as any);
// Act
const { result } = renderHook(() => useUserData(), { wrapper });
// Assert: Use `waitFor` to allow the `useEffect` hook to update the state.
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.watchedItems).toEqual(mockWatchedItems);
expect(result.current.shoppingLists).toEqual(mockShoppingLists);
expect(result.current.error).toBeNull();
});
});
it('should return an error state if one of the fetches fails', async () => {
// Arrange: Simulate a logged-in user.
mockedUseAuth.mockReturnValue({
userProfile: mockUser,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
const mockError = new Error('Failed to fetch watched items');
mockedUseWatchedItemsQuery.mockReturnValue({
data: undefined,
isLoading: false,
error: mockError,
} as any);
mockedUseShoppingListsQuery.mockReturnValue({
data: mockShoppingLists,
isLoading: false,
error: null,
} as any);
// Act
const { result } = renderHook(() => useUserData(), { wrapper });
// Assert
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe('Failed to fetch watched items');
// Data that was fetched successfully should still be populated.
expect(result.current.shoppingLists).toEqual(mockShoppingLists);
// Data that failed should be empty.
expect(result.current.watchedItems).toEqual([]);
});
});
it('should clear data when the user logs out', async () => {
// Arrange: Start with a logged-in user and data.
mockedUseAuth.mockReturnValue({
userProfile: mockUser,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
mockedUseWatchedItemsQuery.mockReturnValue({
data: mockWatchedItems,
isLoading: false,
error: null,
} as any);
mockedUseShoppingListsQuery.mockReturnValue({
data: mockShoppingLists,
isLoading: false,
error: null,
} as any);
const { result, rerender } = renderHook(() => useUserData(), { wrapper });
await waitFor(() => expect(result.current.watchedItems).not.toEqual([]));
// Act: Simulate logout by re-rendering with a null user.
mockedUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
// Update mocks to return empty data for the logged out state
mockedUseWatchedItemsQuery.mockReturnValue({
data: [],
isLoading: false,
error: null,
} as any);
mockedUseShoppingListsQuery.mockReturnValue({
data: [],
isLoading: false,
error: null,
} as any);
rerender();
// Assert: The data should now be cleared.
expect(result.current.watchedItems).toEqual([]);
expect(result.current.shoppingLists).toEqual([]);
});
});