import { afterEach, describe, expect, it, vi } from 'vitest'; import { HostShellComponent } from './host-shell.component'; type FetchMock = ReturnType; function jsonResponse(status: number, body: unknown) { return { ok: status >= 200 && status < 300, status, json: vi.fn().mockResolvedValue(body), } as unknown as Response; } describe('HostShellComponent gameplay wiring', () => { afterEach(() => { vi.restoreAllMocks(); }); it('runs startRound transition and refreshes session details', async () => { const fetchMock: FetchMock = vi .fn() .mockResolvedValueOnce(jsonResponse(201, { ok: true })) .mockResolvedValueOnce( jsonResponse(200, { session: { code: 'ABCD12', status: 'lie', current_round: 2 }, round_question: { id: 41, prompt: 'Q?', answers: [] }, players: [], }) ); vi.stubGlobal('fetch', fetchMock); const component = new HostShellComponent(); component.sessionCode = ' abcd12 '; component.categorySlug = ' history '; await component.startRound(); expect(fetchMock).toHaveBeenNthCalledWith( 1, '/lobby/sessions/ABCD12/rounds/start', expect.objectContaining({ method: 'POST', body: JSON.stringify({ category_slug: 'history' }) }) ); expect(fetchMock).toHaveBeenNthCalledWith( 2, '/lobby/sessions/ABCD12', expect.objectContaining({ method: 'GET' }) ); expect(component.session?.session.status).toBe('lie'); expect(component.roundQuestionId).toBe('41'); expect(component.loading).toBe(false); }); it('captures scoreboard error for retry path', async () => { const fetchMock: FetchMock = vi.fn().mockResolvedValue( jsonResponse(500, { error: 'Scoreboard unavailable' }) ); vi.stubGlobal('fetch', fetchMock); const component = new HostShellComponent(); component.sessionCode = 'ABCD12'; await component.loadScoreboard(); expect(fetchMock).toHaveBeenCalledWith( '/lobby/sessions/ABCD12/scoreboard', expect.objectContaining({ method: 'GET' }) ); expect(component.scoreboardError).toContain('Scoreboard failed: Scoreboard unavailable'); expect(component.loading).toBe(false); }); it('wires showQuestion, mixAnswers and calculateScores with expected request payloads', async () => { const sessionAfterAction = { session: { code: 'ABCD12', status: 'guess', current_round: 1 }, round_question: { id: 77, prompt: 'Q?', answers: [] }, players: [], }; const fetchMock: FetchMock = vi .fn() .mockResolvedValueOnce(jsonResponse(200, { ok: true })) .mockResolvedValueOnce(jsonResponse(200, sessionAfterAction)) .mockResolvedValueOnce(jsonResponse(200, { ok: true })) .mockResolvedValueOnce(jsonResponse(200, sessionAfterAction)) .mockResolvedValueOnce(jsonResponse(200, { ok: true })) .mockResolvedValueOnce(jsonResponse(200, sessionAfterAction)); vi.stubGlobal('fetch', fetchMock); const component = new HostShellComponent(); component.sessionCode = ' abcd12 '; component.roundQuestionId = ' 77 '; await component.showQuestion(); await component.mixAnswers(); await component.calculateScores(); expect(fetchMock).toHaveBeenNthCalledWith( 1, '/lobby/sessions/ABCD12/questions/show', expect.objectContaining({ method: 'POST', body: JSON.stringify({}) }) ); expect(fetchMock).toHaveBeenNthCalledWith( 3, '/lobby/sessions/ABCD12/questions/77/answers/mix', expect.objectContaining({ method: 'POST', body: JSON.stringify({}) }) ); expect(fetchMock).toHaveBeenNthCalledWith( 5, '/lobby/sessions/ABCD12/questions/77/scores/calculate', expect.objectContaining({ method: 'POST', body: JSON.stringify({}) }) ); expect(component.error).toBe(''); expect(component.loading).toBe(false); }); it('runs next-round transition without reload and clears scoreboard payload', async () => { const fetchMock: FetchMock = vi .fn() .mockResolvedValueOnce(jsonResponse(200, { session: { code: 'ABCD12', status: 'lobby', current_round: 2 } })) .mockResolvedValueOnce( jsonResponse(200, { session: { code: 'ABCD12', status: 'lobby', current_round: 2 }, round_question: null, players: [], }) ); vi.stubGlobal('fetch', fetchMock); const component = new HostShellComponent(); component.sessionCode = ' abcd12 '; component.scoreboardPayload = '{"leaderboard":[]}'; component.finalLeaderboardPayload = '{"leaderboard":[{"nickname":"Old","score":1}]}'; component.finalLeaderboard = [{ id: 9, nickname: 'Old', score: 1 }]; await component.startNextRound(); expect(fetchMock).toHaveBeenNthCalledWith( 1, '/lobby/sessions/ABCD12/rounds/next', expect.objectContaining({ method: 'POST', body: JSON.stringify({}) }) ); expect(fetchMock).toHaveBeenNthCalledWith( 2, '/lobby/sessions/ABCD12', expect.objectContaining({ method: 'GET' }) ); expect(component.session?.session.status).toBe('lobby'); expect(component.scoreboardPayload).toBe(''); expect(component.finalLeaderboardPayload).toBe(''); expect(component.finalLeaderboard).toEqual([]); expect(component.nextRoundError).toBe(''); }); it('captures finish-game failure for retry and stores final leaderboard on success', async () => { const fetchMock: FetchMock = vi .fn() .mockResolvedValueOnce(jsonResponse(503, { error: 'Final leaderboard timeout' })) .mockResolvedValueOnce( jsonResponse(200, { session: { code: 'ABCD12', status: 'finished', current_round: 2 }, winner: { id: 1, nickname: 'Luna', score: 320 }, leaderboard: [ { id: 2, nickname: 'Mads', score: 120 }, { id: 1, nickname: 'Luna', score: 320 }, ], }) ) .mockResolvedValueOnce( jsonResponse(200, { session: { code: 'ABCD12', status: 'finished', current_round: 2 }, round_question: null, players: [], }) ); vi.stubGlobal('fetch', fetchMock); const component = new HostShellComponent(); component.sessionCode = 'ABCD12'; await component.finishGame(); expect(component.finishError).toContain('Finish game failed: Final leaderboard timeout'); await component.finishGame(); expect(fetchMock).toHaveBeenNthCalledWith( 2, '/lobby/sessions/ABCD12/finish', expect.objectContaining({ method: 'POST', body: JSON.stringify({}) }) ); expect(component.finishError).toBe(''); expect(component.finalLeaderboardPayload).toContain('\"status\": \"finished\"'); expect(component.finalWinner?.nickname).toBe('Luna'); expect(component.finalLeaderboard.map((entry) => entry.nickname)).toEqual(['Luna', 'Mads']); }); });