161 lines
5.2 KiB
TypeScript
161 lines
5.2 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
createVerticalSliceController,
|
|
type SessionContext,
|
|
type SessionContextStore
|
|
} from '../src/spa/vertical-slice';
|
|
import type { ApiClient } from '../src/api/client';
|
|
|
|
function makeApiMock(overrides?: Partial<ApiClient>): ApiClient {
|
|
const base: ApiClient = {
|
|
health: vi.fn(),
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
joinSession: vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 201,
|
|
data: { player: { id: 9, nickname: 'Maja', session_token: 'token-1', score: 0 }, session: { code: 'ABCD12', status: 'lobby' } }
|
|
}),
|
|
startRound: vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 201,
|
|
data: {
|
|
session: { code: 'ABCD12', status: 'lie', current_round: 1 },
|
|
round: { number: 1, category: { slug: 'history', name: 'History' } }
|
|
}
|
|
})
|
|
};
|
|
|
|
return { ...base, ...overrides };
|
|
}
|
|
|
|
function makeSessionContextStore(initial: SessionContext | null = null): SessionContextStore {
|
|
let value = initial;
|
|
return {
|
|
get: vi.fn(() => value),
|
|
set: vi.fn((next: SessionContext) => {
|
|
value = next;
|
|
})
|
|
};
|
|
}
|
|
|
|
describe('vertical slice controller: lobby -> join -> start round', () => {
|
|
it('tracks loading and success state for join + start flow', async () => {
|
|
const api = makeApiMock();
|
|
const controller = createVerticalSliceController(api);
|
|
|
|
const beforeJoinPromise = controller.joinLobby('abcd12', 'Maja');
|
|
expect(controller.getState().joinState).toBe('loading');
|
|
await beforeJoinPromise;
|
|
|
|
const postJoin = controller.getState();
|
|
expect(postJoin.joinState).toBe('success');
|
|
expect(postJoin.session?.session.code).toBe('ABCD12');
|
|
expect(postJoin.gameplayPhase).toBeNull();
|
|
|
|
const beforeStartPromise = controller.startRound('abcd12', 'history');
|
|
expect(controller.getState().startRoundState).toBe('loading');
|
|
await beforeStartPromise;
|
|
|
|
const postStart = controller.getState();
|
|
expect(postStart.startRoundState).toBe('success');
|
|
});
|
|
|
|
it('persists session context after join and syncs normalized session code', async () => {
|
|
const api = makeApiMock();
|
|
const sessionContextStore = makeSessionContextStore();
|
|
const controller = createVerticalSliceController(api, sessionContextStore);
|
|
|
|
await controller.joinLobby('abcd12', 'Maja');
|
|
|
|
expect(sessionContextStore.set).toHaveBeenCalledWith({
|
|
sessionCode: 'ABCD12',
|
|
playerToken: 'token-1',
|
|
nickname: 'Maja'
|
|
});
|
|
expect(controller.getState().sessionCode).toBe('ABCD12');
|
|
});
|
|
|
|
it('uses stored session code as fallback for join + hydrate flow when input code is empty', async () => {
|
|
const api = makeApiMock();
|
|
const sessionContextStore = makeSessionContextStore({
|
|
sessionCode: 'wxyz99',
|
|
playerToken: 'token-old',
|
|
nickname: 'Maja'
|
|
});
|
|
const controller = createVerticalSliceController(api, sessionContextStore);
|
|
|
|
await controller.joinLobby(' ', 'Maja');
|
|
|
|
expect(api.joinSession).toHaveBeenCalledWith({ code: 'WXYZ99', nickname: 'Maja' });
|
|
expect(api.getSession).toHaveBeenCalledWith('ABCD12');
|
|
});
|
|
|
|
it('surfaces a friendly error when join fails', async () => {
|
|
const api = makeApiMock({
|
|
joinSession: vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 404,
|
|
error: { kind: 'http', status: 404, message: 'HTTP 404', payload: { error: 'Session not found' } }
|
|
})
|
|
});
|
|
|
|
const controller = createVerticalSliceController(api);
|
|
await controller.joinLobby('missing', 'Maja');
|
|
|
|
const state = controller.getState();
|
|
expect(state.joinState).toBe('error');
|
|
expect(state.errorMessage).toContain('Join fejlede');
|
|
});
|
|
|
|
it('surfaces a friendly error when round start fails', async () => {
|
|
const api = makeApiMock({
|
|
startRound: vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 400,
|
|
error: { kind: 'http', status: 400, message: 'HTTP 400', payload: { error: 'Round can only be started from lobby' } }
|
|
})
|
|
});
|
|
|
|
const controller = createVerticalSliceController(api);
|
|
await controller.startRound('ABCD12', 'history');
|
|
|
|
const state = controller.getState();
|
|
expect(state.startRoundState).toBe('error');
|
|
expect(state.errorMessage).toContain('Kunne ikke starte runden');
|
|
});
|
|
});
|