Merge main into PR #164 and resolve SPA shell conflicts
This commit is contained in:
18
docs/spa-cutover-flag.md
Normal file
18
docs/spa-cutover-flag.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# SPA cutover feature flag (`USE_SPA_UI`)
|
||||
|
||||
## Formål
|
||||
`USE_SPA_UI` styrer om host/player UI routes serverer Angular SPA shell eller legacy Django templates.
|
||||
|
||||
## Miljø-toggle (uden kodeændring)
|
||||
Sæt env var pr. miljø:
|
||||
|
||||
- `USE_SPA_UI=true` -> `/lobby/ui/host` og `/lobby/ui/player` returnerer SPA shell
|
||||
- `USE_SPA_UI=false` (default) -> legacy template-flow bruges uændret
|
||||
|
||||
Backward compatibility under cutover:
|
||||
- Hvis `USE_SPA_UI` ikke er sat, bruges `WPP_SPA_ENABLED` som fallback.
|
||||
|
||||
## Verifikation
|
||||
- Flag OFF: `UiScreenTests.test_legacy_templates_are_used_when_spa_flag_is_off`
|
||||
- Flag ON (host): `UiScreenTests.test_host_screen_can_render_angular_shell_when_feature_flag_enabled`
|
||||
- Flag ON (player): `UiScreenTests.test_player_screen_can_render_angular_shell_when_feature_flag_enabled`
|
||||
@@ -1,17 +1,31 @@
|
||||
import type { ApiResult, HealthResponse, SessionDetailResponse } from './types';
|
||||
import type {
|
||||
ApiResult,
|
||||
HealthResponse,
|
||||
JoinSessionRequest,
|
||||
JoinSessionResponse,
|
||||
SessionDetailResponse,
|
||||
StartRoundRequest,
|
||||
StartRoundResponse
|
||||
} from './types';
|
||||
|
||||
export interface ApiClient {
|
||||
health(): Promise<ApiResult<HealthResponse>>;
|
||||
getSession(code: string): Promise<ApiResult<SessionDetailResponse>>;
|
||||
joinSession(payload: JoinSessionRequest): Promise<ApiResult<JoinSessionResponse>>;
|
||||
startRound(code: string, payload: StartRoundRequest): Promise<ApiResult<StartRoundResponse>>;
|
||||
}
|
||||
|
||||
export function createApiClient(baseUrl = '', fetchImpl: typeof fetch = fetch): ApiClient {
|
||||
async function request<T>(path: string): Promise<ApiResult<T>> {
|
||||
async function request<T>(path: string, method: 'GET' | 'POST', payload?: unknown): Promise<ApiResult<T>> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetchImpl(`${baseUrl}${path}`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' }
|
||||
method,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(payload === undefined ? {} : { 'Content-Type': 'application/json' })
|
||||
},
|
||||
...(payload === undefined ? {} : { body: JSON.stringify(payload) })
|
||||
});
|
||||
} catch {
|
||||
return {
|
||||
@@ -21,9 +35,9 @@ export function createApiClient(baseUrl = '', fetchImpl: typeof fetch = fetch):
|
||||
};
|
||||
}
|
||||
|
||||
let payload: unknown;
|
||||
let responsePayload: unknown;
|
||||
try {
|
||||
payload = await response.json();
|
||||
responsePayload = await response.json();
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -40,17 +54,28 @@ export function createApiClient(baseUrl = '', fetchImpl: typeof fetch = fetch):
|
||||
kind: 'http',
|
||||
status: response.status,
|
||||
message: `HTTP ${response.status}`,
|
||||
payload
|
||||
payload: responsePayload
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, status: response.status, data: payload as T };
|
||||
return { ok: true, status: response.status, data: responsePayload as T };
|
||||
}
|
||||
|
||||
return {
|
||||
health: () => request<HealthResponse>('/healthz'),
|
||||
health: () => request<HealthResponse>('/healthz', 'GET'),
|
||||
getSession: (code: string) =>
|
||||
request<SessionDetailResponse>(`/lobby/sessions/${encodeURIComponent(code.trim().toUpperCase())}`)
|
||||
request<SessionDetailResponse>(`/lobby/sessions/${encodeURIComponent(code.trim().toUpperCase())}`, 'GET'),
|
||||
joinSession: (payload: JoinSessionRequest) =>
|
||||
request<JoinSessionResponse>('/lobby/sessions/join', 'POST', {
|
||||
code: payload.code.trim().toUpperCase(),
|
||||
nickname: payload.nickname.trim()
|
||||
}),
|
||||
startRound: (code: string, payload: StartRoundRequest) =>
|
||||
request<StartRoundResponse>(
|
||||
`/lobby/sessions/${encodeURIComponent(code.trim().toUpperCase())}/rounds/start`,
|
||||
'POST',
|
||||
payload
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,9 +31,30 @@ export interface SessionRoundQuestion {
|
||||
}
|
||||
|
||||
export interface PhaseViewModel {
|
||||
phase: string;
|
||||
available_actions: string[];
|
||||
constraints: Record<string, unknown>;
|
||||
status: string;
|
||||
round_number: number;
|
||||
players_count: number;
|
||||
constraints: {
|
||||
min_players_to_start: number;
|
||||
max_players_mvp: number;
|
||||
min_players_reached: boolean;
|
||||
max_players_allowed: boolean;
|
||||
};
|
||||
host: {
|
||||
can_start_round: boolean;
|
||||
can_show_question: boolean;
|
||||
can_mix_answers: boolean;
|
||||
can_calculate_scores: boolean;
|
||||
can_reveal_scoreboard: boolean;
|
||||
can_start_next_round: boolean;
|
||||
can_finish_game: boolean;
|
||||
};
|
||||
player: {
|
||||
can_join: boolean;
|
||||
can_submit_lie: boolean;
|
||||
can_submit_guess: boolean;
|
||||
can_view_final_result: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionDetailResponse {
|
||||
@@ -43,6 +64,43 @@ export interface SessionDetailResponse {
|
||||
phase_view_model: PhaseViewModel;
|
||||
}
|
||||
|
||||
export interface JoinSessionRequest {
|
||||
code: string;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
export interface JoinSessionResponse {
|
||||
player: {
|
||||
id: number;
|
||||
nickname: string;
|
||||
session_token: string;
|
||||
score: number;
|
||||
};
|
||||
session: {
|
||||
code: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StartRoundRequest {
|
||||
category_slug: string;
|
||||
}
|
||||
|
||||
export interface StartRoundResponse {
|
||||
session: {
|
||||
code: string;
|
||||
status: string;
|
||||
current_round: number;
|
||||
};
|
||||
round: {
|
||||
number: number;
|
||||
category: {
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type ApiErrorKind = 'network' | 'http' | 'parse';
|
||||
|
||||
export interface ApiFailure {
|
||||
|
||||
87
frontend/src/spa/vertical-slice.ts
Normal file
87
frontend/src/spa/vertical-slice.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ApiClient } from '../api/client';
|
||||
import type { SessionDetailResponse } from '../api/types';
|
||||
|
||||
export type AsyncState = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
export interface VerticalSliceState {
|
||||
sessionCode: string;
|
||||
session: SessionDetailResponse | null;
|
||||
joinState: AsyncState;
|
||||
startRoundState: AsyncState;
|
||||
loadingSession: boolean;
|
||||
errorMessage: string | null;
|
||||
}
|
||||
|
||||
export interface VerticalSliceController {
|
||||
getState(): VerticalSliceState;
|
||||
hydrateLobby(sessionCode: string): Promise<VerticalSliceState>;
|
||||
joinLobby(sessionCode: string, nickname: string): Promise<VerticalSliceState>;
|
||||
startRound(sessionCode: string, categorySlug: string): Promise<VerticalSliceState>;
|
||||
}
|
||||
|
||||
export function createVerticalSliceController(api: ApiClient): VerticalSliceController {
|
||||
const state: VerticalSliceState = {
|
||||
sessionCode: '',
|
||||
session: null,
|
||||
joinState: 'idle',
|
||||
startRoundState: 'idle',
|
||||
loadingSession: false,
|
||||
errorMessage: null
|
||||
};
|
||||
|
||||
const normalizeCode = (value: string): string => value.trim().toUpperCase();
|
||||
|
||||
async function hydrateLobby(sessionCode: string): Promise<VerticalSliceState> {
|
||||
state.loadingSession = true;
|
||||
state.errorMessage = null;
|
||||
state.sessionCode = normalizeCode(sessionCode);
|
||||
|
||||
const result = await api.getSession(state.sessionCode);
|
||||
state.loadingSession = false;
|
||||
|
||||
if (!result.ok) {
|
||||
state.errorMessage = 'Kunne ikke hente lobby-status.';
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
state.session = result.data;
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
async function joinLobby(sessionCode: string, nickname: string): Promise<VerticalSliceState> {
|
||||
state.joinState = 'loading';
|
||||
state.errorMessage = null;
|
||||
|
||||
const join = await api.joinSession({ code: sessionCode, nickname });
|
||||
if (!join.ok) {
|
||||
state.joinState = 'error';
|
||||
state.errorMessage = 'Join fejlede. Tjek kode eller nickname og prøv igen.';
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
state.joinState = 'success';
|
||||
return hydrateLobby(sessionCode);
|
||||
}
|
||||
|
||||
async function startRound(sessionCode: string, categorySlug: string): Promise<VerticalSliceState> {
|
||||
state.startRoundState = 'loading';
|
||||
state.errorMessage = null;
|
||||
|
||||
const start = await api.startRound(sessionCode, { category_slug: categorySlug });
|
||||
if (!start.ok) {
|
||||
state.startRoundState = 'error';
|
||||
state.errorMessage = 'Kunne ikke starte runden. Opdatér lobbyen og prøv igen.';
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
state.startRoundState = 'success';
|
||||
return hydrateLobby(sessionCode);
|
||||
}
|
||||
|
||||
return {
|
||||
getState: () => ({ ...state }),
|
||||
hydrateLobby,
|
||||
joinLobby,
|
||||
startRound
|
||||
};
|
||||
}
|
||||
@@ -7,21 +7,68 @@ let server: Server;
|
||||
let baseUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||
server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.url === '/healthz') {
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true, service: 'weirsoe-party-protocol' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/lobby/sessions/ABCD12') {
|
||||
if (req.url === '/lobby/sessions/ABCD12' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
session: { code: 'ABCD12', status: 'lobby', host_id: 1, current_round: 1, players_count: 2 },
|
||||
session: { code: 'ABCD12', status: 'lobby', host_id: 1, current_round: 1, players_count: 3 },
|
||||
players: [],
|
||||
round_question: null,
|
||||
phase_view_model: { phase: 'lobby', available_actions: ['start_round'], constraints: {} }
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/lobby/sessions/join' && req.method === 'POST') {
|
||||
res.writeHead(201, { 'content-type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
player: { id: 9, nickname: 'Maja', session_token: 'token-1', score: 0 },
|
||||
session: { code: 'ABCD12', status: 'lobby' }
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/lobby/sessions/ABCD12/rounds/start' && req.method === 'POST') {
|
||||
res.writeHead(201, { 'content-type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
session: { code: 'ABCD12', status: 'lie', current_round: 1 },
|
||||
round: { number: 1, category: { slug: 'history', name: 'History' } }
|
||||
})
|
||||
);
|
||||
return;
|
||||
@@ -59,6 +106,20 @@ describe('createApiClient', () => {
|
||||
expect(session.ok).toBe(true);
|
||||
if (session.ok) {
|
||||
expect(session.data.session.code).toBe('ABCD12');
|
||||
expect(session.data.phase_view_model.host.can_start_round).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('supports join + start round writes for lobby vertical slice', async () => {
|
||||
const client = createApiClient(baseUrl);
|
||||
|
||||
const join = await client.joinSession({ code: 'abcd12', nickname: 'Maja' });
|
||||
expect(join.ok).toBe(true);
|
||||
|
||||
const start = await client.startRound('abcd12', { category_slug: 'history' });
|
||||
expect(start.ok).toBe(true);
|
||||
if (start.ok) {
|
||||
expect(start.data.session.status).toBe('lie');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
115
frontend/tests/vertical-slice.test.ts
Normal file
115
frontend/tests/vertical-slice.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createVerticalSliceController } 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 };
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
const beforeStartPromise = controller.startRound('abcd12', 'history');
|
||||
expect(controller.getState().startRoundState).toBe('loading');
|
||||
await beforeStartPromise;
|
||||
|
||||
const postStart = controller.getState();
|
||||
expect(postStart.startRoundState).toBe('success');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -8,5 +8,5 @@
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["vitest/globals", "node"]
|
||||
},
|
||||
"include": ["src/api", "tests"]
|
||||
"include": ["src", "tests"]
|
||||
}
|
||||
|
||||
6
lobby/feature_flags.py
Normal file
6
lobby/feature_flags.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def use_spa_ui() -> bool:
|
||||
"""Central read-point for SPA cutover flag."""
|
||||
return bool(getattr(settings, "USE_SPA_UI", False))
|
||||
@@ -2,12 +2,12 @@
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WPP SPA Shell</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WPP SPA Shell</title>
|
||||
<link rel="stylesheet" href="{{ spa_asset_base }}/styles.css">
|
||||
</head>
|
||||
<body data-wpp-shell-route="{{ shell_route }}" data-wpp-shell-kind="{{ shell_kind }}">
|
||||
<app-root>Indlæser Angular app-shell…</app-root>
|
||||
<script src="{{ spa_asset_base }}/main.js" type="module"></script>
|
||||
<app-root data-wpp-shell-route="{{ shell_route }}" data-wpp-shell-kind="{{ shell_kind }}">Indlæser Angular app-shell…</app-root>
|
||||
<script type="module" src="{{ spa_asset_base }}/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -973,19 +973,31 @@ class UiScreenTests(TestCase):
|
||||
self.assertContains(response, "player_shell_runtime_error")
|
||||
self.assertContains(response, "window.addEventListener(\"error\"")
|
||||
|
||||
@override_settings(WPP_SPA_ENABLED=True)
|
||||
@override_settings(USE_SPA_UI=False)
|
||||
def test_legacy_templates_are_used_when_spa_flag_is_off(self):
|
||||
self.client.login(username="host_ui", password="secret123")
|
||||
|
||||
host_response = self.client.get(reverse("lobby:host_screen"))
|
||||
player_response = self.client.get(reverse("lobby:player_screen"))
|
||||
|
||||
self.assertContains(host_response, "Host panel")
|
||||
self.assertContains(player_response, "Player panel")
|
||||
self.assertNotContains(host_response, "<app-root")
|
||||
self.assertNotContains(player_response, "<app-root")
|
||||
|
||||
@override_settings(USE_SPA_UI=True)
|
||||
def test_host_screen_can_render_angular_shell_when_feature_flag_enabled(self):
|
||||
self.client.login(username="host_ui", password="secret123")
|
||||
|
||||
response = self.client.get(reverse("lobby:host_screen"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<app-root>")
|
||||
self.assertContains(response, "<app-root")
|
||||
self.assertContains(response, "data-wpp-shell-route=\"/host\"")
|
||||
self.assertContains(response, "data-wpp-shell-kind=\"host\"")
|
||||
self.assertContains(response, "/static/frontend/angular/browser/main.js")
|
||||
|
||||
@override_settings(WPP_SPA_ENABLED=True)
|
||||
@override_settings(USE_SPA_UI=True)
|
||||
def test_host_screen_deeplink_preserves_spa_path_when_feature_flag_enabled(self):
|
||||
self.client.login(username="host_ui", password="secret123")
|
||||
|
||||
@@ -994,27 +1006,27 @@ class UiScreenTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<app-root>")
|
||||
self.assertContains(response, "<app-root")
|
||||
self.assertContains(response, "data-wpp-shell-route=\"/host/guess/round-1\"")
|
||||
self.assertContains(response, "data-wpp-shell-kind=\"host\"")
|
||||
|
||||
@override_settings(WPP_SPA_ENABLED=True)
|
||||
@override_settings(USE_SPA_UI=True)
|
||||
def test_host_screen_deeplink_normalizes_redundant_slashes_when_feature_flag_enabled(self):
|
||||
self.client.login(username="host_ui", password="secret123")
|
||||
|
||||
response = self.client.get("/lobby/ui/host//guess///round-1//")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<app-root>")
|
||||
self.assertContains(response, "<app-root")
|
||||
self.assertContains(response, "data-wpp-shell-route=\"/host/guess/round-1\"")
|
||||
self.assertContains(response, "data-wpp-shell-kind=\"host\"")
|
||||
|
||||
@override_settings(WPP_SPA_ENABLED=True)
|
||||
@override_settings(USE_SPA_UI=True)
|
||||
def test_player_screen_can_render_angular_shell_when_feature_flag_enabled(self):
|
||||
response = self.client.get(reverse("lobby:player_screen"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "<app-root>")
|
||||
self.assertContains(response, "<app-root")
|
||||
self.assertContains(response, "data-wpp-shell-route=\"/player\"")
|
||||
self.assertContains(response, "data-wpp-shell-kind=\"player\"")
|
||||
self.assertContains(response, "/static/frontend/angular/browser/main.js")
|
||||
|
||||
@@ -4,6 +4,8 @@ from django.shortcuts import render
|
||||
|
||||
from fupogfakta.models import Category
|
||||
|
||||
from .feature_flags import use_spa_ui
|
||||
|
||||
|
||||
def _render_spa_shell(request, shell_route: str, shell_kind: str):
|
||||
return render(
|
||||
@@ -19,7 +21,7 @@ def _render_spa_shell(request, shell_route: str, shell_kind: str):
|
||||
|
||||
@login_required
|
||||
def host_screen(request, spa_path=None):
|
||||
if settings.WPP_SPA_ENABLED:
|
||||
if use_spa_ui():
|
||||
host_route = "/host"
|
||||
if spa_path:
|
||||
normalized_spa_path = "/".join(segment for segment in spa_path.split("/") if segment)
|
||||
@@ -32,7 +34,7 @@ def host_screen(request, spa_path=None):
|
||||
|
||||
|
||||
def player_screen(request):
|
||||
if settings.WPP_SPA_ENABLED:
|
||||
if use_spa_ui():
|
||||
return _render_spa_shell(request, "/player", "player")
|
||||
|
||||
return render(request, "lobby/player_screen.html")
|
||||
|
||||
@@ -99,7 +99,11 @@ STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
WPP_SPA_ENABLED = env('WPP_SPA_ENABLED', 'false').lower() == 'true'
|
||||
USE_SPA_UI_RAW = env('USE_SPA_UI')
|
||||
if USE_SPA_UI_RAW is None:
|
||||
# Backward-compatible fallback while cutover is rolling out.
|
||||
USE_SPA_UI_RAW = env('WPP_SPA_ENABLED', 'false')
|
||||
USE_SPA_UI = USE_SPA_UI_RAW.lower() == 'true'
|
||||
WPP_SPA_ASSET_BASE = env('WPP_SPA_ASSET_BASE', '/static/frontend/angular/browser').rstrip('/')
|
||||
|
||||
CHANNEL_REDIS_HOST = env('CHANNEL_REDIS_HOST', '127.0.0.1')
|
||||
|
||||
Reference in New Issue
Block a user