// @vitest-environment jsdom import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, act, waitFor } from '@testing-library/react'; const routeApiMocks = vi.hoisted(() => ({ addScannedCardToOwned: vi.fn().mockResolvedValue(undefined), addScannedCardToCollection: vi.fn().mockResolvedValue(undefined), addScannedCardToDeck: vi.fn().mockResolvedValue(undefined), routeScannedCardToDestination: vi.fn().mockResolvedValue(undefined), fetchScannerCollections: vi.fn().mockResolvedValue([]), fetchScannerDecks: vi.fn().mockResolvedValue([]), fetchBatchOwnership: vi.fn().mockResolvedValue({}), createScannerCollection: vi.fn(), })); vi.mock('../../lib/scanner-route-api.js', () => ({ addScannedCardToOwned: routeApiMocks.addScannedCardToOwned, addScannedCardToCollection: routeApiMocks.addScannedCardToCollection, addScannedCardToDeck: routeApiMocks.addScannedCardToDeck, routeScannedCardToDestination: routeApiMocks.routeScannedCardToDestination, fetchScannerCollections: routeApiMocks.fetchScannerCollections, fetchScannerDecks: routeApiMocks.fetchScannerDecks, fetchBatchOwnership: routeApiMocks.fetchBatchOwnership, createScannerCollection: routeApiMocks.createScannerCollection, })); import { useScannerQueue } from '../../lib/use-scanner-queue.js'; import { clearScannerCartStorage } from '../../lib/scanner-session.js'; const { addScannedCardToOwned, routeScannedCardToDestination, } = routeApiMocks; const scanDefaults = { condition: 'NM', isFoil: false }; const user = { userId: 1, email: 'test@example.com', role: 'user' }; describe('useScannerQueue — cart model', () => { beforeEach(() => { vi.clearAllMocks(); sessionStorage.clear(); }); afterEach(() => { clearScannerCartStorage(); }); it('handleCardScanned enqueues locally without route helpers', async () => { const { result } = renderHook(() => useScannerQueue({ user, sessionDestination: { type: 'owned', id: null, label: 'My Collection' }, scanDefaults, deckMode: null, }) ); await act(async () => { result.current.handleCardScanned({ name: 'Pikachu', set: 'Base', databaseId: 101, }); }); expect(routeScannedCardToDestination).not.toHaveBeenCalled(); expect(addScannedCardToOwned).not.toHaveBeenCalled(); expect(result.current.scannedCards).toHaveLength(1); const card = result.current.scannedCards[0]; expect(card).toMatchObject({ name: 'Pikachu', set: 'Base', processed: false, }); expect(card.id).toBeTruthy(); expect(result.current.selectedCards.has(card.id)).toBe(true); }); it('handleBulkAction owned commits each selected card and removes them from the cart', async () => { const { result } = renderHook(() => useScannerQueue({ user, sessionDestination: null, scanDefaults, deckMode: null, }) ); await act(async () => { result.current.handleCardScanned({ name: 'Bolt', set: 'Alpha', databaseId: 201, }); }); const cardId = result.current.scannedCards[0].id; await act(async () => { await result.current.handleBulkAction('owned'); }); expect(addScannedCardToOwned).toHaveBeenCalledTimes(1); expect(addScannedCardToOwned).toHaveBeenCalledWith( expect.objectContaining({ id: cardId, name: 'Bolt', set: 'Alpha' }) ); expect(result.current.scannedCards).toHaveLength(0); expect(result.current.selectedCards.size).toBe(0); }); it('commitSelectedToOwned returns successful ids instead of reading stale queue state', async () => { const { result } = renderHook(() => useScannerQueue({ user, sessionDestination: null, scanDefaults, deckMode: null, }) ); await act(async () => { result.current.handleCardScanned({ name: 'Jace', set: 'Origins', databaseId: 301, }); }); const cardId = result.current.scannedCards[0].id; let successfulIds; await act(async () => { successfulIds = await result.current.commitSelectedToOwned(); }); expect(successfulIds).toEqual([cardId]); expect(addScannedCardToOwned).toHaveBeenCalledTimes(1); expect(result.current.scannedCards).toHaveLength(0); }); it('commitSelectedToOwned returns an empty array when the API fails', async () => { addScannedCardToOwned.mockRejectedValueOnce(new Error('network')); const { result } = renderHook(() => useScannerQueue({ user, sessionDestination: null, scanDefaults, deckMode: null, }) ); await act(async () => { result.current.handleCardScanned({ name: 'Jace', set: 'Origins', databaseId: 301, }); }); let successfulIds; await act(async () => { successfulIds = await result.current.commitSelectedToOwned(); }); expect(successfulIds).toEqual([]); expect(result.current.scannedCards).toHaveLength(1); }); it('commitSelectedToOwned routes through handleBulkAction owned', async () => { const { result } = renderHook(() => useScannerQueue({ user, sessionDestination: null, scanDefaults, deckMode: null, }) ); await act(async () => { result.current.handleCardScanned({ name: 'Jace', set: 'Origins', databaseId: 301, }); }); await act(async () => { await result.current.commitSelectedToOwned(); }); expect(addScannedCardToOwned).toHaveBeenCalledTimes(1); expect(result.current.scannedCards).toHaveLength(0); }); it('persists cart state to sessionStorage on scan', async () => { const { result } = renderHook(() => useScannerQueue({ user, sessionDestination: null, scanDefaults, deckMode: null, }) ); await act(async () => { result.current.handleCardScanned({ name: 'Liliana', set: 'Midnight', databaseId: 401, }); }); const cardId = result.current.scannedCards[0].id; await waitFor(() => { const raw = sessionStorage.getItem('deckhearth:scanner-cart'); expect(raw).toBeTruthy(); const parsed = JSON.parse(raw); expect(parsed.scannedCards).toHaveLength(1); expect(parsed.selectedCardIds).toEqual([cardId]); }); expect(result.current.unprocessedCount).toBe(1); }); });