fix(lobby): tighten canonical host round flow for issue 287
This commit is contained in:
@@ -85,10 +85,10 @@ function sessionDetailPayload(
|
||||
},
|
||||
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_show_question: false,
|
||||
can_mix_answers: false,
|
||||
can_calculate_scores: false,
|
||||
can_reveal_scoreboard: false,
|
||||
can_start_next_round: status === 'scoreboard',
|
||||
can_finish_game: status === 'scoreboard',
|
||||
},
|
||||
@@ -179,80 +179,16 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
it('runs next-round transition into canonical lie phase and clears prior final leaderboard state', 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 })));
|
||||
.mockResolvedValueOnce(jsonResponse(200, { session: { code: 'ABCD12', status: 'lie', current_round: 2 } }))
|
||||
.mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('lie', { roundQuestionId: 99 })));
|
||||
|
||||
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 }];
|
||||
|
||||
@@ -264,8 +200,8 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
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.session?.session.status).toBe('lie');
|
||||
expect(component.roundQuestionId).toBe('99');
|
||||
expect(component.finalLeaderboardPayload).toBe('');
|
||||
expect(component.finalLeaderboard).toEqual([]);
|
||||
expect(component.nextRoundError).toBe('');
|
||||
@@ -341,25 +277,24 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
|
||||
expect(replaceState).toHaveBeenCalledWith(null, '', '#/host/guess/ABCD12');
|
||||
expect(component.canStartRound).toBe(false);
|
||||
expect(component.canShowQuestion).toBe(false);
|
||||
expect(component.canMixAnswers).toBe(false);
|
||||
expect(component.canCalculateScores).toBe(true);
|
||||
});
|
||||
|
||||
it('uses phase_view_model to keep host action surface phase-specific', async () => {
|
||||
const component = new HostShellComponent();
|
||||
|
||||
expect(component.canStartRound).toBe(true);
|
||||
expect(component.canShowQuestion).toBe(false);
|
||||
|
||||
component.session = sessionDetailPayload('lie') as any;
|
||||
expect(component.canStartRound).toBe(false);
|
||||
expect(component.canShowQuestion).toBe(true);
|
||||
expect(component.canMixAnswers).toBe(true);
|
||||
|
||||
component.session = sessionDetailPayload('reveal') as any;
|
||||
expect(component.canRevealScoreboard).toBe(true);
|
||||
expect(component.canStartNextRound).toBe(false);
|
||||
expect(component.canFinishGame).toBe(false);
|
||||
});
|
||||
|
||||
it('uses phase_view_model to keep host action surface bound to round boundaries only', async () => {
|
||||
const component = new HostShellComponent();
|
||||
|
||||
expect(component.canStartRound).toBe(true);
|
||||
expect(component.canStartNextRound).toBe(false);
|
||||
expect(component.canFinishGame).toBe(false);
|
||||
|
||||
component.session = sessionDetailPayload('lie') as any;
|
||||
expect(component.canStartRound).toBe(false);
|
||||
expect(component.canStartNextRound).toBe(false);
|
||||
expect(component.canFinishGame).toBe(false);
|
||||
|
||||
component.session = sessionDetailPayload('scoreboard') as any;
|
||||
expect(component.canStartNextRound).toBe(true);
|
||||
expect(component.canFinishGame).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user