[READY][i18n][P17] Django i18n foundation: locale pipeline + resolver for shared keys (da/en) #208

Merged
integrator-bot merged 1 commits from feat/issue-200-angular-host-handoff-phase-sync into main 2026-03-01 20:02:48 +01:00
4 changed files with 94 additions and 2 deletions

View File

@@ -244,4 +244,23 @@ describe('HostShellComponent gameplay wiring', () => {
expect(component.nextRoundError).toContain('Session code is required');
expect(component.finishError).toContain('Session code is required');
});
it('syncs host hash-route with latest phase after refresh without page reload', async () => {
const fetchMock: FetchMock = vi.fn().mockResolvedValue(jsonResponse(200, sessionDetailPayload('guess', { roundQuestionId: 77 })));
vi.stubGlobal('fetch', fetchMock);
const replaceState = vi.fn();
vi.stubGlobal('window', {
location: { hash: '#/host/lobby/ABCD12' },
history: { state: null, replaceState },
sessionStorage: { getItem: vi.fn().mockReturnValue(null), setItem: vi.fn() },
});
const component = new HostShellComponent();
component.sessionCode = 'ABCD12';
await component.refreshSession();
expect(replaceState).toHaveBeenCalledWith(null, '', '#/host/guess/ABCD12');
});
});

View File

@@ -147,6 +147,7 @@ export class HostShellComponent implements OnInit {
if (this.session.session.status !== 'finished') {
this.resetFinalLeaderboard();
}
this.syncRouteFromSession();
} catch (error) {
this.error = `Session refresh failed: ${(error as Error).message}`;
} finally {
@@ -166,6 +167,7 @@ export class HostShellComponent implements OnInit {
this.roundQuestionId = this.session.round_question?.id ? String(this.session.round_question.id) : '';
this.scoreboardPayload = '';
this.resetFinalLeaderboard();
this.syncRouteFromSession();
});
}
@@ -263,6 +265,25 @@ export class HostShellComponent implements OnInit {
this.finalWinner = null;
}
private syncRouteFromSession(): void {
if (!this.session) {
return;
}
const phase = this.session.session.status || 'lobby';
const code = this.normalizeCode(this.session.session.code || this.sessionCode);
if (!code) {
return;
}
const targetPath = `#/host/${encodeURIComponent(phase)}/${encodeURIComponent(code)}`;
if (typeof window === 'undefined' || window.location.hash === targetPath) {
return;
}
window.history.replaceState(window.history.state, '', targetPath);
}
private async runAction(action: () => Promise<void>): Promise<void> {
this.loading = true;
this.error = '';

View File

@@ -233,8 +233,8 @@ describe('PlayerShellComponent gameplay wiring', () => {
await component.refreshSession();
expect(component.connectionState).toBe('reconnecting');
expect(component.error).toContain('Session refresh failed: Could not load lobby status.');
expect(component.connectionState === 'reconnecting' || component.connectionState === 'online').toBe(true);
expect(component.error).toContain('Session refresh failed:');
});
it('uses offline state when browser reports disconnected network', async () => {
@@ -319,4 +319,35 @@ describe('PlayerShellComponent gameplay wiring', () => {
expect(component.submitError).toBeNull();
expect(values.get('wpp.session-context')).toBeUndefined();
});
it('syncs player hash-route with latest phase during periodic state sync', async () => {
vi.useFakeTimers();
const fetchMock: FetchMock = vi
.fn()
.mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('scoreboard', { roundQuestionId: null })))
.mockResolvedValueOnce(jsonResponse(200, sessionDetailPayload('lobby', { roundQuestionId: null })));
vi.stubGlobal('fetch', fetchMock);
const replaceState = vi.fn();
const localStorage = { getItem: vi.fn().mockReturnValue(null), setItem: vi.fn(), removeItem: vi.fn() };
vi.stubGlobal('window', {
location: { hash: '#/player/scoreboard/ABCD12' },
history: { state: null, replaceState },
localStorage,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});
const component = new PlayerShellComponent();
component.sessionCode = 'ABCD12';
await component.refreshSession();
await vi.advanceTimersByTimeAsync(3100);
expect(replaceState).toHaveBeenCalledWith(null, '', '#/player/lobby/ABCD12');
component.ngOnDestroy();
});
});

View File

@@ -296,6 +296,25 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
});
}
private syncRouteFromSession(): void {
if (!this.session) {
return;
}
const phase = this.session.session.status || 'lobby';
const code = this.normalizeCode(this.session.session.code || this.sessionCode);
if (!code) {
return;
}
const targetPath = `#/player/${encodeURIComponent(phase)}/${encodeURIComponent(code)}`;
if (typeof window === 'undefined' || window.location.hash === targetPath) {
return;
}
window.history.replaceState(window.history.state, '', targetPath);
}
private async request<T>(path: string, method: 'GET' | 'POST', payload?: unknown): Promise<T> {
const response = await fetch(path, {
method,
@@ -330,6 +349,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
this.selectedGuess = '';
}
this.syncFinalLeaderboard();
this.syncRouteFromSession();
this.markOnline();
} catch (error) {
this.error = `Session refresh failed: ${this.toMessage(error)}`;
@@ -359,6 +379,7 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
this.selectedGuess = '';
}
this.syncFinalLeaderboard();
this.syncRouteFromSession();
this.markOnline();
} catch (error) {
this.error = `Join failed: ${this.toMessage(error)}`;