fix(frontend): prefer canonical phase for client action gating
This commit is contained in:
@@ -21,6 +21,7 @@ function createFetchRouteMock(handler: FetchRouteHandler): FetchMock {
|
||||
function sessionDetailPayload(
|
||||
status: string,
|
||||
options?: {
|
||||
currentPhase?: string;
|
||||
roundQuestionId?: number | null;
|
||||
reveal?: {
|
||||
correct_answer: string;
|
||||
@@ -81,6 +82,7 @@ function sessionDetailPayload(
|
||||
},
|
||||
phase_view_model: {
|
||||
status,
|
||||
current_phase: options?.currentPhase ?? status,
|
||||
round_number: 1,
|
||||
players_count: 2,
|
||||
constraints: {
|
||||
@@ -89,14 +91,18 @@ function sessionDetailPayload(
|
||||
min_players_reached: true,
|
||||
max_players_allowed: true,
|
||||
},
|
||||
readiness: {
|
||||
question_ready: (options?.currentPhase ?? status) !== 'lobby',
|
||||
scoreboard_ready: (options?.currentPhase ?? status) === 'reveal' || (options?.currentPhase ?? status) === 'scoreboard',
|
||||
},
|
||||
host: {
|
||||
can_start_round: status === 'lobby',
|
||||
can_show_question: status === 'lie',
|
||||
can_mix_answers: status === 'lie' || status === 'guess',
|
||||
can_calculate_scores: status === 'guess',
|
||||
can_reveal_scoreboard: status === 'reveal',
|
||||
can_start_next_round: status === 'reveal',
|
||||
can_finish_game: status === 'reveal',
|
||||
can_start_round: (options?.currentPhase ?? status) === 'lobby',
|
||||
can_show_question: (options?.currentPhase ?? status) === 'lie',
|
||||
can_mix_answers: (options?.currentPhase ?? status) === 'lie' || (options?.currentPhase ?? status) === 'guess',
|
||||
can_calculate_scores: (options?.currentPhase ?? status) === 'guess',
|
||||
can_reveal_scoreboard: (options?.currentPhase ?? status) === 'reveal',
|
||||
can_start_next_round: (options?.currentPhase ?? status) === 'scoreboard',
|
||||
can_finish_game: (options?.currentPhase ?? status) === 'scoreboard',
|
||||
},
|
||||
player: {
|
||||
can_join: status === 'lobby',
|
||||
@@ -259,7 +265,7 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
component.scoreboardPayload = '{"leaderboard":[]}';
|
||||
component.finalLeaderboardPayload = '{"leaderboard":[{"nickname":"Old","score":1}]}' ;
|
||||
component.finalLeaderboard = [{ id: 9, nickname: 'Old', score: 1 }];
|
||||
component.session = sessionDetailPayload('reveal', { roundQuestionId: 77 }) as any;
|
||||
component.session = sessionDetailPayload('scoreboard', { roundQuestionId: 77 }) as any;
|
||||
|
||||
await component.startNextRound();
|
||||
|
||||
@@ -296,7 +302,7 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
|
||||
const component = new HostShellComponent();
|
||||
component.sessionCode = 'ABCD12';
|
||||
component.session = sessionDetailPayload('reveal', { roundQuestionId: 77 }) as any;
|
||||
component.session = sessionDetailPayload('scoreboard', { roundQuestionId: 77 }) as any;
|
||||
|
||||
await component.finishGame();
|
||||
expect(component.finishError).toContain('Finish game failed: Final leaderboard timeout');
|
||||
@@ -320,7 +326,7 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
|
||||
const component = new HostShellComponent();
|
||||
component.sessionCode = ' ';
|
||||
component.session = sessionDetailPayload('reveal', { roundQuestionId: 77 }) as any;
|
||||
component.session = sessionDetailPayload('scoreboard', { roundQuestionId: 77 }) as any;
|
||||
|
||||
await component.startNextRound();
|
||||
await component.finishGame();
|
||||
@@ -351,6 +357,10 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
for (const status of ['lie', 'guess', 'scoreboard'] as const) {
|
||||
component.session = sessionDetailPayload(status, { roundQuestionId: 77 }) as any;
|
||||
await component.loadScoreboard();
|
||||
}
|
||||
|
||||
for (const status of ['lie', 'guess', 'reveal'] as const) {
|
||||
component.session = sessionDetailPayload(status, { roundQuestionId: 77 }) as any;
|
||||
await component.startNextRound();
|
||||
await component.finishGame();
|
||||
}
|
||||
@@ -361,16 +371,42 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
component.session = sessionDetailPayload('reveal', { roundQuestionId: 77 }) as any;
|
||||
expect(component.canCalculateScores).toBe(false);
|
||||
expect(component.canLoadScoreboard).toBe(true);
|
||||
expect(component.canStartNextRound).toBe(true);
|
||||
expect(component.canFinishGame).toBe(true);
|
||||
expect(component.canStartNextRound).toBe(false);
|
||||
expect(component.canFinishGame).toBe(false);
|
||||
|
||||
component.session = sessionDetailPayload('scoreboard', { roundQuestionId: 77 }) as any;
|
||||
expect(component.canLoadScoreboard).toBe(false);
|
||||
expect(component.canStartNextRound).toBe(false);
|
||||
expect(component.canFinishGame).toBe(false);
|
||||
expect(component.canStartNextRound).toBe(true);
|
||||
expect(component.canFinishGame).toBe(true);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prefers canonical current_phase for reveal panel and host routing when status lags behind', async () => {
|
||||
const fetchMock: FetchMock = vi.fn().mockResolvedValue(
|
||||
jsonResponse(200, sessionDetailPayload('reveal', { currentPhase: 'scoreboard', roundQuestionId: 77, reveal: { correct_answer: 'Mercury' } }))
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const replaceState = vi.fn();
|
||||
vi.stubGlobal('window', {
|
||||
location: { hash: '#/host/reveal/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(component.gameplayPhase).toBe('scoreboard');
|
||||
expect(component.showRevealPanel).toBe(true);
|
||||
expect(component.canLoadScoreboard).toBe(false);
|
||||
expect(component.canStartNextRound).toBe(true);
|
||||
expect(component.canFinishGame).toBe(true);
|
||||
expect(replaceState).toHaveBeenCalledWith(null, '', '#/host/scoreboard/ABCD12');
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -408,12 +444,12 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
|
||||
component.session = sessionDetailPayload('reveal') as any;
|
||||
expect(component.canLoadScoreboard).toBe(true);
|
||||
expect(component.canStartNextRound).toBe(true);
|
||||
expect(component.canFinishGame).toBe(true);
|
||||
expect(component.canStartNextRound).toBe(false);
|
||||
expect(component.canFinishGame).toBe(false);
|
||||
|
||||
component.session = sessionDetailPayload('scoreboard') as any;
|
||||
expect(component.canLoadScoreboard).toBe(false);
|
||||
expect(component.canStartNextRound).toBe(false);
|
||||
expect(component.canFinishGame).toBe(false);
|
||||
expect(component.canStartNextRound).toBe(true);
|
||||
expect(component.canFinishGame).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user