144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { createVerticalSliceController } from '../src/spa/vertical-slice';
|
|
import { createSessionContextStore, type StorageLike } from '../src/spa/session-context-store';
|
|
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 makeMemoryStorage(): StorageLike {
|
|
const memory = new Map<string, string>();
|
|
return {
|
|
getItem: (key: string) => memory.get(key) ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
memory.set(key, value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
memory.delete(key);
|
|
}
|
|
};
|
|
}
|
|
|
|
describe('vertical slice controller: lobby -> join -> start round', () => {
|
|
it('tracks loading and success state for join + start flow and stores session context', async () => {
|
|
const api = makeApiMock();
|
|
const store = createSessionContextStore(makeMemoryStorage());
|
|
const controller = createVerticalSliceController(api, store);
|
|
|
|
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.context).toEqual({ sessionCode: 'ABCD12', playerId: 9, token: 'token-1' });
|
|
|
|
const beforeStartPromise = controller.startRound('', 'history');
|
|
expect(controller.getState().startRoundState).toBe('loading');
|
|
await beforeStartPromise;
|
|
|
|
const postStart = controller.getState();
|
|
expect(postStart.startRoundState).toBe('success');
|
|
expect(api.startRound).toHaveBeenCalledWith('ABCD12', { category_slug: 'history' });
|
|
});
|
|
|
|
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');
|
|
});
|
|
|
|
it('returns explicit error when hydrate has no session code in input or context', async () => {
|
|
const api = makeApiMock();
|
|
const controller = createVerticalSliceController(api);
|
|
|
|
await controller.hydrateLobby(' ');
|
|
|
|
const state = controller.getState();
|
|
expect(state.errorMessage).toContain('Session-kode mangler');
|
|
expect(api.getSession).not.toHaveBeenCalled();
|
|
});
|
|
});
|