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; } function sessionDetailPayload(status: string, options?: { roundQuestionId?: number | null }) { const roundQuestionId = options?.roundQuestionId ?? 41; return { session: { code: 'ABCD12', status, host_id: 1, current_round: status === 'lobby' ? 2 : 1, players_count: 2, }, round_question: roundQuestionId === null ? null : { id: roundQuestionId, round_number: 1, prompt: 'Q?', shown_at: '2026-01-01T00:00:00Z', answers: [], }, players: [ { id: 1, nickname: 'Host', score: 0, is_connected: true }, { id: 2, nickname: 'Mads', score: 120, is_connected: true }, ], phase_view_model: { status, round_number: 1, players_count: 2, constraints: { min_players_to_start: 2, max_players_mvp: 8, min_players_reached: true, max_players_allowed: true, }, host: { can_start_round: status === 'lobby', can_show_question: status === 'lie', can_mix_answers: status === 'lie', can_calculate_scores: status === 'guess', can_reveal_scoreboard: status === 'reveal', can_start_next_round: status === 'scoreboard', can_finish_game: status === 'scoreboard', }, player: { can_join: status === 'lobby', can_submit_lie: status === 'lie', can_submit_guess: status === 'guess', can_view_final_result: status === 'finished', }, }, }; } describe('HostShellComponent gameplay wiring', () => { afterEach(() => { vi.restoreAllMocks(); }); it('runs startRound transition and refreshes session details', async () => { const fetchMock: FetchMock = vi .fn() .mockResolvedValueOnce( jsonResponse(201, { session: { code: 'ABCD12', status: 'lie', current_round: 1 }, round: { number: 1, category: { slug: 'history', name: 'History' } }, }) ) .mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('lie'))); 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 fetchMock: FetchMock = vi .fn() .mockResolvedValueOnce( jsonResponse(200, { round_question: { id: 77, round_number: 1, prompt: 'Q?', shown_at: '2026-01-01T00:00:00Z', lie_deadline_at: '2026-01-01T00:00:45Z', }, config: { lie_seconds: 45 }, }) ) .mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('lie', { roundQuestionId: 77 }))) .mockResolvedValueOnce( jsonResponse(200, { session: { code: 'ABCD12', status: 'guess', current_round: 1 }, round_question: { id: 77, round_number: 1 }, answers: [{ text: 'A' }], }) ) .mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('guess', { roundQuestionId: 77 }))) .mockResolvedValueOnce( jsonResponse(200, { session: { code: 'ABCD12', status: 'reveal', current_round: 1 }, round_question: { id: 77, round_number: 1 }, events_created: 2, leaderboard: [{ id: 1, nickname: 'Luna', score: 320 }], }) ) .mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('reveal', { roundQuestionId: 77 }))); vi.stubGlobal('fetch', fetchMock); const component = new HostShellComponent(); component.sessionCode = ' abcd12 '; component.roundQuestionId = ' 77 '; await component.showQuestion(); await component.mixAnswers(); await component.calculateScores(); 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, sessionDetailPayload('lobby', { roundQuestionId: null }))); 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, sessionDetailPayload('finished', { roundQuestionId: null }))); 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']); }); it('guards next-round and finish actions when session code is missing', async () => { const fetchMock: FetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); const component = new HostShellComponent(); component.sessionCode = ' '; await component.startNextRound(); await component.finishGame(); expect(fetchMock).not.toHaveBeenCalled(); expect(component.nextRoundError).toContain('Session code is required'); expect(component.finishError).toContain('Session code is required'); }); it('syncs host hash-route with latest phase after refresh without page reload', async () => { const fetchMock: FetchMock = vi.fn().mockResolvedValue(jsonResponse(200, sessionDetailPayload('guess', { roundQuestionId: 77 }))); vi.stubGlobal('fetch', fetchMock); const replaceState = vi.fn(); vi.stubGlobal('window', { location: { hash: '#/host/lobby/ABCD12' }, history: { state: null, replaceState }, sessionStorage: { getItem: vi.fn().mockReturnValue(null), setItem: vi.fn() }, }); const component = new HostShellComponent(); component.sessionCode = 'ABCD12'; await component.refreshSession(); expect(replaceState).toHaveBeenCalledWith(null, '', '#/host/guess/ABCD12'); }); });