diff --git a/frontend/src/spa/vertical-slice.ts b/frontend/src/spa/vertical-slice.ts index a63bf93..a89b18b 100644 --- a/frontend/src/spa/vertical-slice.ts +++ b/frontend/src/spa/vertical-slice.ts @@ -41,10 +41,10 @@ export function createVerticalSliceController( api: ApiClient, sessionContextStore: SessionContextStore = NOOP_SESSION_CONTEXT_STORE ): VerticalSliceController { - const persistedContext = sessionContextStore.get(); + let contextState = sessionContextStore.get(); const state: VerticalSliceState = { - sessionCode: persistedContext?.sessionCode ?? '', + sessionCode: contextState?.sessionCode ?? '', session: null, gameplayPhase: null, joinState: 'idle', @@ -55,13 +55,22 @@ export function createVerticalSliceController( const normalizeCode = (value: string): string => value.trim().toUpperCase(); + const syncContext = (next: SessionContext): void => { + contextState = next; + sessionContextStore.set(next); + }; + + const resolveSessionCode = (sessionCode: string): string => { + const normalizedRequestedCode = normalizeCode(sessionCode); + const fallbackCode = normalizeCode(state.sessionCode || contextState?.sessionCode || ''); + return normalizedRequestedCode || fallbackCode; + }; + async function hydrateLobby(sessionCode: string): Promise { state.loadingSession = true; state.errorMessage = null; - const normalizedRequestedCode = normalizeCode(sessionCode); - const fallbackCode = normalizeCode(state.sessionCode || persistedContext?.sessionCode || ''); - state.sessionCode = normalizedRequestedCode || fallbackCode; + state.sessionCode = resolveSessionCode(sessionCode); const result = await api.getSession(state.sessionCode); state.loadingSession = false; @@ -76,8 +85,8 @@ export function createVerticalSliceController( state.gameplayPhase = deriveGameplayPhase(result.data); state.sessionCode = normalizeCode(result.data.session.code); - if (persistedContext && state.sessionCode === normalizeCode(persistedContext.sessionCode)) { - sessionContextStore.set({ ...persistedContext, sessionCode: state.sessionCode }); + if (contextState) { + syncContext({ ...contextState, sessionCode: state.sessionCode }); } return { ...state }; @@ -87,9 +96,7 @@ export function createVerticalSliceController( state.joinState = 'loading'; state.errorMessage = null; - const normalizedRequestedCode = normalizeCode(sessionCode); - const fallbackCode = normalizeCode(state.sessionCode || persistedContext?.sessionCode || ''); - const requestCode = normalizedRequestedCode || fallbackCode; + const requestCode = resolveSessionCode(sessionCode); const join = await api.joinSession({ code: requestCode, nickname }); if (!join.ok) { @@ -101,7 +108,7 @@ export function createVerticalSliceController( state.joinState = 'success'; state.sessionCode = normalizeCode(join.data.session.code || requestCode); - sessionContextStore.set({ + syncContext({ sessionCode: state.sessionCode, playerToken: join.data.player.session_token, nickname: join.data.player.nickname @@ -114,9 +121,7 @@ export function createVerticalSliceController( state.startRoundState = 'loading'; state.errorMessage = null; - const normalizedRequestedCode = normalizeCode(sessionCode); - const fallbackCode = normalizeCode(state.sessionCode || persistedContext?.sessionCode || ''); - const codeToUse = normalizedRequestedCode || fallbackCode; + const codeToUse = resolveSessionCode(sessionCode); const start = await api.startRound(codeToUse, { category_slug: categorySlug }); if (!start.ok) { diff --git a/frontend/tests/vertical-slice.test.ts b/frontend/tests/vertical-slice.test.ts index ef1e004..5f9c4ad 100644 --- a/frontend/tests/vertical-slice.test.ts +++ b/frontend/tests/vertical-slice.test.ts @@ -124,6 +124,67 @@ describe('vertical slice controller: lobby -> join -> start round', () => { expect(api.getSession).toHaveBeenCalledWith('ABCD12'); }); + it('uses stored session code for startRound when input code is empty after join', async () => { + const api = makeApiMock(); + const sessionContextStore = makeSessionContextStore(); + const controller = createVerticalSliceController(api, sessionContextStore); + + await controller.joinLobby('abcd12', 'Maja'); + await controller.startRound('', 'history'); + + expect(api.startRound).toHaveBeenCalledWith('ABCD12', { category_slug: 'history' }); + }); + + it('keeps joined player context when hydrate syncs normalized session code', async () => { + const api = makeApiMock({ + getSession: vi.fn().mockResolvedValue({ + ok: true, + status: 200, + data: { + session: { code: 'abcd12', status: 'lobby', host_id: 1, current_round: 1, players_count: 3 }, + players: [], + round_question: null, + phase_view_model: { + status: 'lobby', + round_number: 1, + players_count: 3, + constraints: { + min_players_to_start: 3, + max_players_mvp: 5, + min_players_reached: true, + max_players_allowed: true + }, + host: { + can_start_round: true, + can_show_question: false, + can_mix_answers: false, + can_calculate_scores: false, + can_reveal_scoreboard: false, + can_start_next_round: false, + can_finish_game: false + }, + player: { + can_join: true, + can_submit_lie: false, + can_submit_guess: false, + can_view_final_result: false + } + } + } + }) + }); + const sessionContextStore = makeSessionContextStore(); + const controller = createVerticalSliceController(api, sessionContextStore); + + await controller.joinLobby('abcd12', 'Maja'); + + expect(sessionContextStore.set).toHaveBeenLastCalledWith({ + sessionCode: 'ABCD12', + playerToken: 'token-1', + nickname: 'Maja' + }); + }); + it('surfaces a friendly error when join fails', async () => { const api = makeApiMock({ joinSession: vi.fn().mockResolvedValue({