import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter, Route, Routes } from 'react-router'; import { beforeEach, describe, expect, it, vi } from 'vitest'; // The completion modal waits for the loop-closing animation; there is none // to wait for in jsdom, and these tests are about the content, not the delay. vi.mock('@/lib/motion', async (importOriginal) => ({ ...(await importOriginal()), prefersReducedMotion: () => true, })); import { useAuthStore } from '@/stores/auth-store'; import { useVersusStore } from '@/stores/versus-store'; import VersusCompletionModal from './VersusCompletionModal'; import type { ScoreBreakdown } from '@/types'; /** * The multiplayer completion modal. Seeded via `setState` on the real * versus/auth stores rather than mocked, per WP-19 convention - the component * reads several fields together and the derived `waitingForOpponent` / * `matchResult` branches are the behaviour under test. */ const score = (totalScore: number): ScoreBreakdown => ({ baseScore: 0, chainLength: 0, linkCount: 0, chainLengthBonus: 0, timeBonus: 0, obscurityBonus: 0, hintPenalty: 0, duplicatePenalty: 0, elapsedSeconds: 0, totalScore, }); function renderModal(initialEntry = '/versus/lobby/m1') { return render( } /> versus route} /> , ); } beforeEach(() => { useVersusStore.setState({ lobbyState: 'idle', matchResult: null, myScore: null, }); useAuthStore.setState({ user: null }); }); describe('VersusCompletionModal', () => { it('renders nothing before the local player has finished', () => { renderModal(); expect(screen.queryByRole('dialog')).toBeNull(); }); it('shows the waiting state once I finish but the opponent has not', () => { useVersusStore.setState({ myScore: score(1500), lobbyState: 'playing' }); renderModal(); expect(screen.getByRole('dialog', { name: 'You finished' })).toBeInTheDocument(); expect(screen.getByText('1,500')).toBeInTheDocument(); expect(screen.getByText('Waiting for opponent to finish…')).toBeInTheDocument(); }); it('defaults myTotal to 0 when the score object has no totalScore field', () => { useVersusStore.setState({ myScore: {} as ScoreBreakdown, lobbyState: 'playing', }); renderModal(); expect(screen.getByText('0')).toBeInTheDocument(); }); it('lets a stuck player leave the waiting state, resetting and returning to Versus', async () => { useVersusStore.setState({ myScore: score(1500), lobbyState: 'playing' }); renderModal(); await userEvent.click(screen.getByRole('button', { name: 'Back to Versus' })); expect(useVersusStore.getState().myScore).toBeNull(); expect(screen.getByText('versus route')).toBeInTheDocument(); }); it('renders an empty dialog when finished with no match result yet', () => { useVersusStore.setState({ myScore: score(1500), lobbyState: 'finished', matchResult: null }); renderModal(); expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(screen.queryByText('Match complete')).toBeNull(); }); it('declares a tie when the match has no winner', () => { useAuthStore.setState({ user: { id: 'u1', email: 'a@b.com', username: 'Me', isAdmin: false } }); useVersusStore.setState({ myScore: score(1500), lobbyState: 'finished', matchResult: { winnerId: null, player1: { id: 'u1', username: 'Me' }, player2: { id: 'u2', username: 'Rival' }, player1Score: score(1500), player2Score: score(1500), }, }); renderModal(); expect(screen.getByRole('heading', { name: /Match complete/ })).toBeInTheDocument(); expect(screen.getByText('A tie')).toBeInTheDocument(); expect(screen.getByText('Me')).toBeInTheDocument(); expect(screen.getByText('Rival')).toBeInTheDocument(); }); it('declares a win for the signed-in player when they are the winner', () => { useAuthStore.setState({ user: { id: 'u1', email: 'a@b.com', username: 'Me', isAdmin: false } }); useVersusStore.setState({ myScore: score(2000), lobbyState: 'finished', matchResult: { winnerId: 'u1', player1: { id: 'u1', username: 'Me' }, player2: { id: 'u2', username: 'Rival' }, player1Score: score(2000), player2Score: score(900), }, }); renderModal(); expect(screen.getByText('You won')).toBeInTheDocument(); }); it('declares a loss when the winner is the other player', () => { useAuthStore.setState({ user: { id: 'u1', email: 'a@b.com', username: 'Me', isAdmin: false } }); useVersusStore.setState({ myScore: score(900), lobbyState: 'finished', matchResult: { winnerId: 'u2', player1: { id: 'u1', username: 'Me' }, player2: { id: 'u2', username: 'Rival' }, player1Score: score(900), player2Score: score(2000), }, }); renderModal(); expect(screen.getByText('You lost')).toBeInTheDocument(); }); it('defaults a missing player score to 0', () => { useVersusStore.setState({ myScore: score(0), lobbyState: 'finished', matchResult: { winnerId: null, player1: { id: 'u1', username: 'Me' }, player2: { id: 'u2', username: 'Rival' }, player1Score: null, player2Score: null, }, }); renderModal(); const zeros = screen.getAllByText('0'); expect(zeros.length).toBeGreaterThanOrEqual(2); }); it('returns to Versus from the finished result', async () => { useVersusStore.setState({ myScore: score(1500), lobbyState: 'finished', matchResult: { winnerId: null, player1: { id: 'u1', username: 'Me' }, player2: { id: 'u2', username: 'Rival' }, player1Score: score(1500), player2Score: score(1500), }, }); renderModal(); await userEvent.click(screen.getByRole('button', { name: 'Back to Versus' })); expect(screen.getByText('versus route')).toBeInTheDocument(); }); });