Merge pull request '[MVP][frontend] #249 Angular-first SPA foundation: host/player shell + API client skeleton' (#253) from dev/issue-249-angular-spa-foundation into main
Some checks failed
CI / test-and-quality (push) Has been cancelled

This commit was merged in pull request #253.
This commit is contained in:
2026-03-02 02:38:02 +01:00
3 changed files with 110 additions and 1 deletions

View File

@@ -1,7 +1,15 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withHashLocation } from '@angular/router';
import { routes } from './app.routes';
import { createWppApiClient, WPP_API_CLIENT } from './wpp-api-client';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes, withHashLocation())],
providers: [
provideRouter(routes, withHashLocation()),
{
provide: WPP_API_CLIENT,
useFactory: () => createWppApiClient(),
},
],
};

View File

@@ -0,0 +1,43 @@
import { describe, expect, it, vi } from 'vitest';
import { createWppApiClient } from './wpp-api-client';
function jsonResponse(status: number, body: unknown) {
return {
ok: status >= 200 && status < 300,
status,
json: vi.fn().mockResolvedValue(body),
} as unknown as Response;
}
describe('WPP Angular API client skeleton', () => {
it('normalizes host/player API calls through fetch transport', async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(jsonResponse(200, { session: { code: 'ABCD12', status: 'lobby', host_id: 1, current_round: 1, players_count: 1 }, players: [], round_question: null, phase_view_model: { status: 'lobby', round_number: 1, players_count: 1, constraints: { min_players_to_start: 2, max_players_mvp: 8, min_players_reached: false, max_players_allowed: true }, host: { can_start_round: false, 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 } } }))
.mockResolvedValueOnce(jsonResponse(201, { player: { id: 1, nickname: 'Luna', session_token: 'tok', score: 0 }, session: { code: 'ABCD12', status: 'lobby' } }));
const client = createWppApiClient(fetchMock);
const session = await client.getSession(' abcd12 ');
const joined = await client.joinSession({ code: ' abcd12 ', nickname: ' Luna ' });
expect(session.ok).toBe(true);
expect(joined.ok).toBe(true);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/lobby/sessions/ABCD12',
expect.objectContaining({ method: 'GET', credentials: 'same-origin' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/lobby/sessions/join',
expect.objectContaining({
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({ code: 'ABCD12', nickname: 'Luna' }),
})
);
});
});

View File

@@ -0,0 +1,58 @@
import { InjectionToken } from '@angular/core';
import {
createAngularApiClient,
type AngularApiClient,
type AngularHttpClientLike,
} from '../../../src/api/angular-client';
export const WPP_API_CLIENT = new InjectionToken<AngularApiClient>('WPP_API_CLIENT');
export interface FetchLike {
(input: string, init?: RequestInit): Promise<Response>;
}
export function createFetchHttpClient(fetchImpl: FetchLike): AngularHttpClientLike {
return {
async get<T>(url: string): Promise<T> {
const response = await fetchImpl(url, {
method: 'GET',
headers: { Accept: 'application/json' },
credentials: 'same-origin',
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw {
status: response.status,
message: (payload as { error?: string }).error ?? `HTTP ${response.status}`,
error: payload,
};
}
return payload as T;
},
async post<T>(url: string, body: unknown): Promise<T> {
const response = await fetchImpl(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
credentials: 'same-origin',
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw {
status: response.status,
message: (payload as { error?: string }).error ?? `HTTP ${response.status}`,
error: payload,
};
}
return payload as T;
},
};
}
export function createWppApiClient(fetchImpl: FetchLike = fetch.bind(globalThis)): AngularApiClient {
return createAngularApiClient(createFetchHttpClient(fetchImpl));
}