/** * Component: Audiobook Card Tests * Documentation: documentation/frontend/components.md */ // @vitest-environment jsdom import React from 'react'; import { act, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const createRequestMock = vi.hoisted(() => vi.fn()); const authState = { user: null as null | { id: string; username: string }, }; vi.mock('@/contexts/AuthContext', () => ({ useAuth: () => authState, })); vi.mock('@/lib/hooks/useRequests', () => ({ useCreateRequest: () => ({ createRequest: createRequestMock, isLoading: false }), })); vi.mock('@/components/audiobooks/AudiobookDetailsModal', () => ({ AudiobookDetailsModal: ({ isOpen }: { isOpen: boolean }) => (
), })); vi.mock('next/image', () => ({ __esModule: true, default: (props: any) => , })); const baseAudiobook = { asin: 'asin-1', title: 'Test Book', author: 'Author', }; describe('AudiobookCard', () => { beforeEach(() => { authState.user = null; createRequestMock.mockReset(); vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('disables requests when no user is logged in', async () => { const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render(); const requestButton = screen.getByRole('button', { name: 'Sign in to Request' }); expect(requestButton).toBeDisabled(); expect(createRequestMock).not.toHaveBeenCalled(); }); it('creates a request and shows a success toast', async () => { authState.user = { id: 'user-1', username: 'user' }; createRequestMock.mockResolvedValueOnce(undefined); const onRequestSuccess = vi.fn(); const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render(); fireEvent.click(screen.getByRole('button', { name: 'Request' })); const requestPromise = createRequestMock.mock.results[0]?.value; await act(async () => { await requestPromise; }); expect(createRequestMock).toHaveBeenCalledWith(baseAudiobook); expect(onRequestSuccess).toHaveBeenCalled(); expect(screen.getByText(/Request created!/)).toBeInTheDocument(); await act(async () => { vi.advanceTimersByTime(3000); }); expect(screen.queryByText(/Request created!/)).toBeNull(); }); it('shows in-library state when available', async () => { const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render(); expect(screen.getByText('In Your Library')).toBeInTheDocument(); }); it('opens the details modal when the title is clicked', async () => { const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render(); expect(screen.getByTestId('details-modal')).toHaveAttribute('data-open', 'false'); fireEvent.click(screen.getByText('Test Book')); expect(screen.getByTestId('details-modal')).toHaveAttribute('data-open', 'true'); }); it('shows processing state for downloaded requests', async () => { const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render( ); // Processing status is shown as a div overlay, not a button expect(screen.getByText('Processing')).toBeInTheDocument(); }); it('shows pending status for awaiting_approval requests', async () => { const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render( ); // Card shows "Requested" for all pending statuses expect(screen.getByText('Requested')).toBeInTheDocument(); }); it('allows re-requesting for denied status', async () => { authState.user = { id: 'user-1', username: 'user' }; const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render( ); // Denied status allows re-requesting, so Request button is shown expect(screen.getByRole('button', { name: 'Request' })).toBeInTheDocument(); }); it('shows an error when a request fails', async () => { authState.user = { id: 'user-1', username: 'user' }; createRequestMock.mockRejectedValueOnce(new Error('Request failed')); const { AudiobookCard } = await import('@/components/audiobooks/AudiobookCard'); render(); fireEvent.click(screen.getByRole('button', { name: 'Request' })); const requestPromise = createRequestMock.mock.results[0]?.value; await act(async () => { try { await requestPromise; } catch { // Expected for this test. } }); expect(screen.getByText('Request failed')).toBeInTheDocument(); await act(async () => { vi.advanceTimersByTime(5000); }); expect(screen.queryByText('Request failed')).toBeNull(); }); });