import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Lobby } from '../../components/Lobby'; vi.mock('react-hot-toast', () => ({ default: { success: vi.fn(), error: vi.fn(), }, })); vi.mock('framer-motion', () => ({ motion: { div: ({ children, ...props }: React.PropsWithChildren>) =>
{children}
, }, AnimatePresence: ({ children }: React.PropsWithChildren) => <>{children}, })); vi.mock('qrcode.react', () => ({ QRCodeSVG: ({ value, size }: { value: string; size: number }) => ( ), })); describe('Lobby', () => { const defaultProps = { quizTitle: 'Test Quiz', players: [], gamePin: 'ABC123', role: 'HOST' as const, onStart: vi.fn(), onEndGame: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); Object.defineProperty(window, 'location', { value: { origin: 'http://localhost:5173' }, writable: true, }); }); describe('copy join link', () => { it('shows copy link button for host', () => { render(); expect(screen.getByTitle('Copy join link')).toBeInTheDocument(); }); it('does not show copy link button for client', () => { render(); expect(screen.queryByTitle('Copy join link')).not.toBeInTheDocument(); }); it('copies join URL to clipboard when clicked', async () => { const user = userEvent.setup(); render(); const copyButton = screen.getByTitle('Copy join link'); expect(copyButton.querySelector('.lucide-link')).toBeInTheDocument(); await user.click(copyButton); await waitFor(() => { expect(copyButton.querySelector('.lucide-check')).toBeInTheDocument(); }); }); it('shows success toast when link is copied', async () => { const toast = await import('react-hot-toast'); const user = userEvent.setup(); render(); await user.click(screen.getByTitle('Copy join link')); await waitFor(() => { expect(toast.default.success).toHaveBeenCalledWith('Join link copied!'); }); }); it('shows checkmark after copying link', async () => { const user = userEvent.setup(); render(); const copyButton = screen.getByTitle('Copy join link'); await user.click(copyButton); await waitFor(() => { expect(copyButton.querySelector('.lucide-check')).toBeInTheDocument(); }); }); }); describe('display', () => { it('displays game PIN', () => { render(); const pins = screen.getAllByText('ABC123'); expect(pins.length).toBeGreaterThan(0); }); it('displays quiz title on all screen sizes', () => { render(); const titles = screen.getAllByText('Test Quiz'); expect(titles.length).toBeGreaterThan(0); }); it('displays player count', () => { const players = [ { id: '1', name: 'Player 1', score: 0, avatarSeed: 0.1 }, { id: '2', name: 'Player 2', score: 0, avatarSeed: 0.2 }, ]; render(); const counts = screen.getAllByText('2'); expect(counts.length).toBeGreaterThan(0); }); it('shows waiting message when no players have joined', () => { render(); expect(screen.getByText('Waiting for players to join...')).toBeInTheDocument(); }); it('displays player names when players join', () => { const players = [ { id: '1', name: 'Alice', score: 0, avatarSeed: 0.1 }, { id: '2', name: 'Bob', score: 0, avatarSeed: 0.2 }, ]; render(); expect(screen.getByText('Alice')).toBeInTheDocument(); expect(screen.getByText('Bob')).toBeInTheDocument(); }); }); describe('host controls', () => { it('shows Start button for host', () => { render(); expect(screen.getByRole('button', { name: 'Start' })).toBeInTheDocument(); }); it('disables Start button when no players', () => { render(); expect(screen.getByRole('button', { name: 'Start' })).toBeDisabled(); }); it('enables Start button when players joined', () => { const players = [{ id: '1', name: 'Player 1', score: 0, avatarSeed: 0.1 }]; render(); expect(screen.getByRole('button', { name: 'Start' })).not.toBeDisabled(); }); it('calls onStart when Start clicked', async () => { const user = userEvent.setup(); const players = [{ id: '1', name: 'Player 1', score: 0, avatarSeed: 0.1 }]; render(); await user.click(screen.getByRole('button', { name: 'Start' })); expect(defaultProps.onStart).toHaveBeenCalled(); }); it('shows End Game button when onEndGame provided', () => { render(); expect(screen.getByText('End Game')).toBeInTheDocument(); }); it('calls onEndGame when End Game clicked', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByText('End Game')); expect(defaultProps.onEndGame).toHaveBeenCalled(); }); }); describe('client view', () => { it('shows waiting message for client', () => { const players = [ { id: 'player-1', name: 'My Name', score: 0, avatarSeed: 0.5 }, ]; render(); expect(screen.getByText('Waiting for the host to start...')).toBeInTheDocument(); }); it('shows player name when currentPlayerId matches', () => { const players = [ { id: 'player-1', name: 'My Name', score: 0, avatarSeed: 0.5 }, ]; render( ); expect(screen.getByText('My Name')).toBeInTheDocument(); }); it('does not show Start button for client', () => { render(); expect(screen.queryByRole('button', { name: 'Start' })).not.toBeInTheDocument(); }); }); describe('QR code', () => { it('shows QR code for host', () => { render(); const qrCode = screen.getByTestId('qr-code'); expect(qrCode).toBeInTheDocument(); expect(qrCode).toHaveAttribute('data-value', 'http://localhost:5173/play/ABC123'); }); it('does not show QR code for client', () => { render(); expect(screen.queryByTestId('qr-code')).not.toBeInTheDocument(); }); it('does not show QR code when gamePin is null', () => { render(); expect(screen.queryByTestId('qr-code')).not.toBeInTheDocument(); }); it('generates correct QR code URL with game PIN', () => { render(); const qrCode = screen.getByTestId('qr-code'); expect(qrCode).toHaveAttribute('data-value', 'http://localhost:5173/play/XYZ789'); }); }); describe('host participates mode', () => { it('shows host in player list when hostParticipates is true', () => { const players = [ { id: 'host', name: 'Host Player', score: 0, avatarSeed: 0.99 }, ]; render(); expect(screen.getByText('Host Player')).toBeInTheDocument(); expect(screen.getByText('HOST')).toBeInTheDocument(); }); it('does not show waiting message when host participates and no other players', () => { const players = [ { id: 'host', name: 'Host Player', score: 0, avatarSeed: 0.99 }, ]; render(); expect(screen.queryByText('Waiting for players to join...')).not.toBeInTheDocument(); }); }); describe('presenter feature - host view', () => { const playersWithPresenter = [ { id: 'player-1', name: 'Alice', score: 0, avatarSeed: 0.1 }, { id: 'player-2', name: 'Bob', score: 0, avatarSeed: 0.2 }, ]; it('shows presenter selection hint when host does not participate', () => { render( ); expect(screen.getByText(/Click a player to make them presenter/)).toBeInTheDocument(); }); it('does not show presenter selection hint when host participates', () => { const players = [ { id: 'host', name: 'Host Player', score: 0, avatarSeed: 0.99 }, ...playersWithPresenter, ]; render( ); expect(screen.queryByText(/Click a player to make them presenter/)).not.toBeInTheDocument(); }); it('shows PRESENTER badge on the presenter player', () => { render( ); expect(screen.getByText('PRESENTER')).toBeInTheDocument(); }); it('calls onSetPresenter when clicking a player', async () => { const user = userEvent.setup(); const onSetPresenter = vi.fn(); render( ); await user.click(screen.getByText('Bob')); expect(onSetPresenter).toHaveBeenCalledWith('player-2'); }); it('does not call onSetPresenter when clicking player if host participates', async () => { const user = userEvent.setup(); const onSetPresenter = vi.fn(); const players = [ { id: 'host', name: 'Host Player', score: 0, avatarSeed: 0.99 }, ...playersWithPresenter, ]; render( ); await user.click(screen.getByText('Alice')); expect(onSetPresenter).not.toHaveBeenCalled(); }); it('does not show presenter hint when no players have joined', () => { render( ); expect(screen.queryByText(/Click a player to make them presenter/)).not.toBeInTheDocument(); }); it('applies visual highlight to presenter player', () => { render( ); // The presenter badge exists, confirming visual distinction const presenterBadge = screen.getByText('PRESENTER'); expect(presenterBadge).toHaveClass('bg-yellow-400'); }); }); describe('presenter feature - client view', () => { it('shows presenter crown and badge when current player is presenter', () => { const players = [ { id: 'player-1', name: 'My Name', score: 0, avatarSeed: 0.5 }, ]; render( ); expect(screen.getByText('You are the Presenter')).toBeInTheDocument(); expect(screen.getByText('You can advance screens during the game')).toBeInTheDocument(); }); it('shows waiting message when not presenter', () => { const players = [ { id: 'player-1', name: 'My Name', score: 0, avatarSeed: 0.5 }, { id: 'player-2', name: 'Other', score: 0, avatarSeed: 0.6 }, ]; render( ); expect(screen.queryByText('You are the Presenter')).not.toBeInTheDocument(); expect(screen.getByText('Waiting for the host to start...')).toBeInTheDocument(); }); it('shows waiting message when no presenter assigned', () => { const players = [ { id: 'player-1', name: 'My Name', score: 0, avatarSeed: 0.5 }, ]; render( ); expect(screen.queryByText('You are the Presenter')).not.toBeInTheDocument(); expect(screen.getByText('Waiting for the host to start...')).toBeInTheDocument(); }); }); describe('presenter feature - edge cases', () => { it('handles undefined presenterId gracefully', () => { const players = [ { id: 'player-1', name: 'Alice', score: 0, avatarSeed: 0.1 }, ]; // Should not throw render( ); expect(screen.queryByText('PRESENTER')).not.toBeInTheDocument(); }); it('handles onSetPresenter being undefined', async () => { const user = userEvent.setup(); const players = [ { id: 'player-1', name: 'Alice', score: 0, avatarSeed: 0.1 }, ]; // Should not throw when clicking render( ); // Click should not cause error await user.click(screen.getByText('Alice')); // No assertion needed - just testing it doesn't crash }); it('presenter badge only appears once even with multiple players', () => { const players = [ { id: 'player-1', name: 'Alice', score: 0, avatarSeed: 0.1 }, { id: 'player-2', name: 'Bob', score: 0, avatarSeed: 0.2 }, { id: 'player-3', name: 'Charlie', score: 0, avatarSeed: 0.3 }, ]; render( ); const presenterBadges = screen.getAllByText('PRESENTER'); expect(presenterBadges).toHaveLength(1); }); it('updates presenter display when presenterId changes', () => { const players = [ { id: 'player-1', name: 'Alice', score: 0, avatarSeed: 0.1 }, { id: 'player-2', name: 'Bob', score: 0, avatarSeed: 0.2 }, ]; const { rerender } = render( ); // Initially Alice is presenter expect(screen.getByText('PRESENTER')).toBeInTheDocument(); // Change presenter to Bob rerender( ); // Still exactly one presenter badge const presenterBadges = screen.getAllByText('PRESENTER'); expect(presenterBadges).toHaveLength(1); }); }); });