import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from '../../src/lib/queryClient'; import * as useRomsModule from '../../src/hooks/useRoms'; import Roms from '../../src/routes/roms'; import { RomFile } from '../../src/types/rom'; // Mock the useRoms hooks vi.spyOn(useRomsModule, 'useRoms'); vi.spyOn(useRomsModule, 'useScanDirectory'); vi.spyOn(useRomsModule, 'useEnrichMetadata'); vi.spyOn(useRomsModule, 'useLinkGameToRom'); vi.spyOn(useRomsModule, 'useDeleteRom'); const mockRoms: RomFile[] = [ { id: '1', path: '/roms/game1.zip', filename: 'game1.zip', checksum: 'abc123def456', size: 1024000, format: 'zip', status: 'active', addedAt: '2026-01-01T00:00:00Z', game: { id: 'g1', title: 'Game One', slug: 'game-one', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', }, }, { id: '2', path: '/roms/game2.rar', filename: 'game2.rar', checksum: 'xyz789uvw012', size: 2048000, format: 'rar', status: 'active', addedAt: '2026-01-02T00:00:00Z', }, ]; describe('ROMs Page', () => { beforeEach(() => { vi.clearAllMocks(); // Default mocks vi.mocked(useRomsModule.useRoms).mockReturnValue({ data: mockRoms, isLoading: false, error: null, } as any); vi.mocked(useRomsModule.useScanDirectory).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); vi.mocked(useRomsModule.useEnrichMetadata).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); vi.mocked(useRomsModule.useLinkGameToRom).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); vi.mocked(useRomsModule.useDeleteRom).mockReturnValue({ mutateAsync: vi.fn(), isPending: false, } as any); }); it('should render empty state when no roms', () => { vi.mocked(useRomsModule.useRoms).mockReturnValue({ data: [], isLoading: false, error: null, } as any); render( ); expect(screen.getByText(/no roms yet/i)).toBeInTheDocument(); }); it('should render loading state', () => { vi.mocked(useRomsModule.useRoms).mockReturnValue({ data: undefined, isLoading: true, error: null, } as any); render( ); expect(screen.getByText(/loading roms/i)).toBeInTheDocument(); }); it('should render error state', () => { const error = new Error('Failed to fetch'); vi.mocked(useRomsModule.useRoms).mockReturnValue({ data: undefined, isLoading: false, error, } as any); render( ); expect(screen.getByText(/error/i)).toBeInTheDocument(); expect(screen.getByText(/failed to fetch/i)).toBeInTheDocument(); }); it('should render table with roms', () => { render( ); expect(screen.getByText('game1.zip')).toBeInTheDocument(); expect(screen.getByText('game2.rar')).toBeInTheDocument(); }); it('should render "Scan Directory" button', () => { render( ); expect(screen.getByRole('button', { name: /scan directory/i })).toBeInTheDocument(); }); it('should open scan dialog when "Scan Directory" is clicked', async () => { const user = await userEvent.setup(); render( ); const scanButton = screen.getByRole('button', { name: /scan directory/i }); await user.click(scanButton); await waitFor(() => { expect(screen.getByText(/scan roms directory/i)).toBeInTheDocument(); }); }); it('should render rom with linked game', () => { render( ); expect(screen.getByText('Game One')).toBeInTheDocument(); }); it('should render "Link Metadata" button for rom without game', () => { render( ); // game2.rar doesn't have a linked game const linkButtons = screen.getAllByRole('button', { name: /link metadata/i }); expect(linkButtons.length).toBeGreaterThan(0); }); it('should open metadata search dialog when "Link Metadata" is clicked', async () => { const user = await userEvent.setup(); render( ); const linkButton = screen.getAllByRole('button', { name: /link metadata/i })[0]; await user.click(linkButton); await waitFor(() => { expect(screen.getByText(/search metadata/i)).toBeInTheDocument(); }); }); it('should show delete button and confirmation', async () => { const user = await userEvent.setup(); render( ); const deleteButtons = screen.getAllByRole('button', { name: /delete/i }); expect(deleteButtons.length).toBeGreaterThan(0); }); it('should handle table columns correctly', () => { render( ); // Check for table headers - be more specific to avoid matching data cells const table = screen.getByRole('table'); expect(table.querySelector('th:nth-child(1)')).toHaveTextContent(/filename/i); expect(table.querySelector('th:nth-child(2)')).toHaveTextContent(/size/i); expect(table.querySelector('th:nth-child(3)')).toHaveTextContent(/checksum/i); expect(table.querySelector('th:nth-child(4)')).toHaveTextContent(/status/i); expect(table.querySelector('th:nth-child(5)')).toHaveTextContent(/game/i); expect(table.querySelector('th:nth-child(6)')).toHaveTextContent(/actions/i); }); it('should display file size in human readable format', () => { render( ); // 1024000 bytes should be displayed as 1000 KB expect(screen.getByText(/1000\s*kb/i)).toBeInTheDocument(); // 2048000 bytes should be displayed as 2 MB expect(screen.getByText(/2\s*mb/i)).toBeInTheDocument(); }); it('should display checksum truncated with ellipsis', () => { render( ); // First 8 chars should be shown + ... expect(screen.getByText(/abc123de\.\.\./)).toBeInTheDocument(); }); });