diff --git a/docs/ISSUE-180-SPA-NEXT-ROUND-FINAL-LEADERBOARD-EVIDENCE.md b/docs/ISSUE-180-SPA-NEXT-ROUND-FINAL-LEADERBOARD-EVIDENCE.md index 80758ee..cffc004 100644 --- a/docs/ISSUE-180-SPA-NEXT-ROUND-FINAL-LEADERBOARD-EVIDENCE.md +++ b/docs/ISSUE-180-SPA-NEXT-ROUND-FINAL-LEADERBOARD-EVIDENCE.md @@ -3,7 +3,7 @@ ## Flow log (Angular SPA) 1. Host reaches `reveal` phase and runs `loadScoreboard()` (`GET /lobby/sessions/:code/scoreboard`). 2. Host can start next round directly in SPA via `startNextRound()` (`POST /lobby/sessions/:code/rounds/next`) and then session hydrate (`GET /lobby/sessions/:code`) without page reload. -3. Host can finish game directly in SPA via `finishGame()` (`POST /lobby/sessions/:code/finish`), rendering final leaderboard payload in the same shell. +3. Host can finish game directly in SPA via `finishGame()` (`POST /lobby/sessions/:code/finish`), rendering winner + sorted final leaderboard (plus raw payload for debug) in the same shell. 4. Player SPA renders final leaderboard from refreshed finished-session payload (sorted by score desc, nickname tiebreak) without leaving SPA route. 5. Error/retry paths are implemented and covered: - scoreboard failure -> `scoreboardError` + retry button 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 53fdaf6..3a2156d 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 @@ -135,6 +135,8 @@ describe('HostShellComponent gameplay wiring', () => { 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(); @@ -150,6 +152,8 @@ describe('HostShellComponent gameplay wiring', () => { ); expect(component.session?.session.status).toBe('lobby'); expect(component.scoreboardPayload).toBe(''); + expect(component.finalLeaderboardPayload).toBe(''); + expect(component.finalLeaderboard).toEqual([]); expect(component.nextRoundError).toBe(''); }); @@ -161,7 +165,10 @@ describe('HostShellComponent gameplay wiring', () => { jsonResponse(200, { session: { code: 'ABCD12', status: 'finished', current_round: 2 }, winner: { id: 1, nickname: 'Luna', score: 320 }, - leaderboard: [{ id: 1, nickname: 'Luna', score: 320 }], + leaderboard: [ + { id: 2, nickname: 'Mads', score: 120 }, + { id: 1, nickname: 'Luna', score: 320 }, + ], }) ) .mockResolvedValueOnce( @@ -189,6 +196,8 @@ describe('HostShellComponent gameplay wiring', () => { expect.objectContaining({ method: 'POST', body: JSON.stringify({}) }) ); expect(component.finishError).toBe(''); - expect(component.finalLeaderboardPayload).toContain('"status": "finished"'); + expect(component.finalLeaderboardPayload).toContain('\"status\": \"finished\"'); + expect(component.finalWinner?.nickname).toBe('Luna'); + expect(component.finalLeaderboard.map((entry) => entry.nickname)).toEqual(['Luna', 'Mads']); }); }); diff --git a/frontend/angular/src/app/features/host/host-shell.component.ts b/frontend/angular/src/app/features/host/host-shell.component.ts index b14efcd..12f4927 100644 --- a/frontend/angular/src/app/features/host/host-shell.component.ts +++ b/frontend/angular/src/app/features/host/host-shell.component.ts @@ -11,10 +11,16 @@ interface SessionDetail { players: Array<{ id: number; nickname: string; score: number }>; } +interface LeaderboardEntry { + id: number; + nickname: string; + score: number; +} + interface LeaderboardResponse { session: { code: string; status: string; current_round: number }; - leaderboard: Array<{ id: number; nickname: string; score: number }>; - winner?: { id: number; nickname: string; score: number } | null; + leaderboard: LeaderboardEntry[]; + winner?: LeaderboardEntry | null; } @Component({ @@ -53,6 +59,13 @@ interface LeaderboardResponse {
{{ scoreboardPayload }}
+ Winner: {{ finalWinner.nickname }} ({{ finalWinner.score }} pts)
+{{ finalLeaderboardPayload }}
`,
@@ -68,6 +81,8 @@ export class HostShellComponent {
finishError = '';
scoreboardPayload = '';
finalLeaderboardPayload = '';
+ finalLeaderboard: LeaderboardEntry[] = [];
+ finalWinner: LeaderboardEntry | null = null;
session: SessionDetail | null = null;
private readonly controller = createVerticalSliceController(createApiClient());
@@ -110,7 +125,7 @@ export class HostShellComponent {
this.sessionCode = this.session.session.code;
this.roundQuestionId = this.session.round_question?.id ? String(this.session.round_question.id) : '';
if (this.session.session.status !== 'finished') {
- this.finalLeaderboardPayload = '';
+ this.resetFinalLeaderboard();
}
} catch (error) {
this.error = `Session refresh failed: ${(error as Error).message}`;
@@ -129,7 +144,7 @@ export class HostShellComponent {
this.sessionCode = this.session.session.code;
this.roundQuestionId = this.session.round_question?.id ? String(this.session.round_question.id) : '';
this.scoreboardPayload = '';
- this.finalLeaderboardPayload = '';
+ this.resetFinalLeaderboard();
});
}
@@ -183,7 +198,7 @@ export class HostShellComponent {
const code = this.normalizeCode(this.sessionCode);
await this.request(`/lobby/sessions/${encodeURIComponent(code)}/rounds/next`, 'POST', {});
this.scoreboardPayload = '';
- this.finalLeaderboardPayload = '';
+ this.resetFinalLeaderboard();
await this.refreshSession();
} catch (error) {
this.nextRoundError = `Next round failed: ${(error as Error).message}`;
@@ -200,6 +215,13 @@ export class HostShellComponent {
const code = this.normalizeCode(this.sessionCode);
const payload = await this.request