From 55fc7583895f3e95ec865dd8e9119145a7745461 Mon Sep 17 00:00:00 2001 From: DEV-bot Date: Mon, 16 Mar 2026 13:33:49 +0000 Subject: [PATCH] test(gameplay): stabilize canonical host gating specs --- .../host/host-shell.component.spec.ts | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/frontend/angular/src/app/features/host/host-shell.component.spec.ts b/frontend/angular/src/app/features/host/host-shell.component.spec.ts index 8535bfd..d57ff4d 100644 --- a/frontend/angular/src/app/features/host/host-shell.component.spec.ts +++ b/frontend/angular/src/app/features/host/host-shell.component.spec.ts @@ -4,6 +4,8 @@ import { HostShellComponent } from './host-shell.component'; type FetchMock = ReturnType; +type FetchRouteHandler = (input: RequestInfo | URL, init?: RequestInit) => Response | Promise; + function jsonResponse(status: number, body: unknown) { return { ok: status >= 200 && status < 300, @@ -12,6 +14,10 @@ function jsonResponse(status: number, body: unknown) { } as unknown as Response; } +function createFetchRouteMock(handler: FetchRouteHandler): FetchMock { + return vi.fn((input: RequestInfo | URL, init?: RequestInit) => Promise.resolve(handler(input, init))); +} + function sessionDetailPayload( status: string, options?: { @@ -180,14 +186,33 @@ describe('HostShellComponent gameplay wiring', () => { }); it('wires showQuestion, mixAnswers and calculateScores with canonical phase gating', async () => { - const fetchMock: FetchMock = vi - .fn() - .mockResolvedValueOnce(jsonResponse(200, { session: { code: 'ABCD12', status: 'lie', current_round: 2 } })) - .mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('lie', { roundQuestionId: 99 }))) - .mockResolvedValueOnce(jsonResponse(200, { session: { code: 'ABCD12', status: 'guess', current_round: 2 } })) - .mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('guess', { roundQuestionId: 77 }))) - .mockResolvedValueOnce(jsonResponse(200, { session: { code: 'ABCD12', status: 'reveal', current_round: 2 } })) - .mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('reveal', { roundQuestionId: 77 }))); + let refreshCount = 0; + const fetchMock = createFetchRouteMock((input, init) => { + const url = String(input); + const method = init?.method ?? 'GET'; + + if (method === 'POST' && url === '/lobby/sessions/ABCD12/questions/show') { + return jsonResponse(200, { session: { code: 'ABCD12', status: 'lie', current_round: 2 } }); + } + if (method === 'POST' && url === '/lobby/sessions/ABCD12/questions/99/answers/mix') { + return jsonResponse(200, { session: { code: 'ABCD12', status: 'guess', current_round: 2 } }); + } + if (method === 'POST' && url === '/lobby/sessions/ABCD12/questions/77/scores/calculate') { + return jsonResponse(200, { session: { code: 'ABCD12', status: 'reveal', current_round: 2 } }); + } + if (method === 'GET' && url === '/lobby/sessions/ABCD12') { + refreshCount += 1; + if (refreshCount === 1) { + return jsonResponse(200, sessionDetailPayload('lie', { roundQuestionId: 99 })); + } + if (refreshCount === 2) { + return jsonResponse(200, sessionDetailPayload('guess', { roundQuestionId: 77 })); + } + return jsonResponse(200, sessionDetailPayload('reveal', { roundQuestionId: 77 })); + } + + throw new Error(`Unhandled fetch in test: ${method} ${url}`); + }); vi.stubGlobal('fetch', fetchMock); @@ -197,21 +222,35 @@ describe('HostShellComponent gameplay wiring', () => { component.session = sessionDetailPayload('lie', { roundQuestionId: null }) as any; await component.showQuestion(); + expect(component.session?.session.status).toBe('lie'); + expect(component.roundQuestionId).toBe('99'); component.session = sessionDetailPayload('guess', { roundQuestionId: 77 }) as any; await component.mixAnswers(); + expect(component.session?.session.status).toBe('guess'); + await component.calculateScores(); + expect(component.session?.session.status).toBe('reveal'); expect(component.error).toBe(''); expect(component.loading).toBe(false); expect(fetchMock).toHaveBeenCalledTimes(6); }); it('runs next-round transition without reload and clears scoreboard payload', async () => { - const fetchMock: FetchMock = vi - .fn() - .mockResolvedValueOnce(jsonResponse(200, { session: { code: 'ABCD12', status: 'lie', current_round: 2 } })) - .mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('lie', { roundQuestionId: 99 }))); + const fetchMock = createFetchRouteMock((input, init) => { + const url = String(input); + const method = init?.method ?? 'GET'; + + if (method === 'POST' && url === '/lobby/sessions/ABCD12/rounds/next') { + return jsonResponse(200, { session: { code: 'ABCD12', status: 'lie', current_round: 2 } }); + } + if (method === 'GET' && url === '/lobby/sessions/ABCD12') { + return jsonResponse(200, sessionDetailPayload('lie', { roundQuestionId: 99 })); + } + + throw new Error(`Unhandled fetch in test: ${method} ${url}`); + }); vi.stubGlobal('fetch', fetchMock); @@ -363,14 +402,17 @@ describe('HostShellComponent gameplay wiring', () => { component.session = sessionDetailPayload('lie') as any; expect(component.canStartRound).toBe(false); + expect(component.canShowQuestion).toBe(true); expect(component.canStartNextRound).toBe(false); expect(component.canFinishGame).toBe(false); component.session = sessionDetailPayload('reveal') as any; + expect(component.canLoadScoreboard).toBe(true); expect(component.canStartNextRound).toBe(true); expect(component.canFinishGame).toBe(true); component.session = sessionDetailPayload('scoreboard') as any; + expect(component.canLoadScoreboard).toBe(false); expect(component.canStartNextRound).toBe(false); expect(component.canFinishGame).toBe(false); });