From e5b8081c104fb75529e8d18fd9858ef6b079a51f Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Fri, 13 Mar 2026 09:51:09 +0000 Subject: [PATCH] test: add issue 278 locale and audio smoke gate --- docs/ISSUE-278-SMOKE-E2E-GATE-ARTIFACT.md | 36 ++++++++++++ .../src/app/i18n-mvp-flow-smoke.spec.ts | 58 +++++++++++-------- 2 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 docs/ISSUE-278-SMOKE-E2E-GATE-ARTIFACT.md diff --git a/docs/ISSUE-278-SMOKE-E2E-GATE-ARTIFACT.md b/docs/ISSUE-278-SMOKE-E2E-GATE-ARTIFACT.md new file mode 100644 index 0000000..361b3d3 --- /dev/null +++ b/docs/ISSUE-278-SMOKE-E2E-GATE-ARTIFACT.md @@ -0,0 +1,36 @@ +# Issue #278 Artifact — smoke/e2e gate for da+en locale flow and primary-only audio + +## Scope +Acceptance for `[READY][#175][P4]`: +1. Verify one MVP host+player smoke run in `en`. +2. Verify one MVP host+player smoke run in `da`. +3. Verify audio routing remains `primary-device only` so phone/player clients never take playback ownership. + +Dette er en gate-/evidensleverance. Ingen ny produktfunktion ud over test/verifikation. + +## Implemented smoke gate +Angular smoke spec: `frontend/angular/src/app/i18n-mvp-flow-smoke.spec.ts` + +The gate now runs two explicit locale scenarios: +- `en`: host refresh/start-round copy + player submit-guess copy +- `da`: samme flow med dansk copy + +Audio-policy delen af samme smoke-spec verificerer: +- host/primary playback path er uændret før player mount +- player mount installerer no-audio guard på secondary device +- guard fjernes igen ved unmount, så primary path fortsat er eneste aktive output + +## Recommended verification command +Køres fra `frontend/angular`: + +```bash +npm test -- --run src/app/i18n-mvp-flow-smoke.spec.ts src/app/lobby-i18n.spec.ts src/app/features/player/player-shell.component.spec.ts +``` + +## Why this is the gate +- `i18n-mvp-flow-smoke.spec.ts` giver en lille, samlet smoke/e2e-lignende verifikation af host+player i begge locale-kontekster. +- `lobby-i18n.spec.ts` holder shared locale propagation + contract fallback grøn. +- `player-shell.component.spec.ts` dækker den dybere regressionflade for audio-guard på secondary device. + +## Conclusion +Gate’en verificerer nu eksplicit begge locale-runs (`da` + `en`) og bekræfter primary-only audio routing i MVP-flowet. diff --git a/frontend/angular/src/app/i18n-mvp-flow-smoke.spec.ts b/frontend/angular/src/app/i18n-mvp-flow-smoke.spec.ts index 23754af..84b8fd0 100644 --- a/frontend/angular/src/app/i18n-mvp-flow-smoke.spec.ts +++ b/frontend/angular/src/app/i18n-mvp-flow-smoke.spec.ts @@ -4,44 +4,56 @@ import { HostShellComponent } from './features/host/host-shell.component'; import { PlayerShellComponent } from './features/player/player-shell.component'; import { setPreferredLocale } from './lobby-i18n'; +function stubShellGlobals(initialLocale: string) { + vi.stubGlobal('window', { + location: { hash: '', search: '' }, + history: { state: null, replaceState: vi.fn() }, + localStorage: { getItem: vi.fn().mockReturnValue(initialLocale), setItem: vi.fn(), removeItem: vi.fn() }, + sessionStorage: { getItem: vi.fn().mockReturnValue(null), setItem: vi.fn(), removeItem: vi.fn() }, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }); + vi.stubGlobal('navigator', { language: `${initialLocale}-US`, onLine: true }); +} + describe('i18n MVP flow smoke (host/player + audio policy)', () => { afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); - it('resolves host/player copy in en and da from shared catalog', () => { - vi.stubGlobal('window', { - location: { hash: '', search: '' }, - history: { state: null, replaceState: vi.fn() }, - localStorage: { getItem: vi.fn().mockReturnValue('en'), setItem: vi.fn(), removeItem: vi.fn() }, - sessionStorage: { getItem: vi.fn().mockReturnValue(null), setItem: vi.fn(), removeItem: vi.fn() }, - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - }); - vi.stubGlobal('navigator', { language: 'en-US', onLine: true }); + it.each([ + { + locale: 'en', + hostRefresh: 'Refresh', + hostStartRound: 'Start round', + playerSubmitGuess: 'Submit guess', + }, + { + locale: 'da', + hostRefresh: 'Opdatér', + hostStartRound: 'Start runde', + playerSubmitGuess: 'Send gæt', + }, + ])('resolves one host/player locale run for $locale', ({ locale, hostRefresh, hostStartRound, playerSubmitGuess }) => { + stubShellGlobals(locale); const host = new HostShellComponent(); const player = new PlayerShellComponent(); host.ngOnInit(); player.ngOnInit(); + setPreferredLocale(locale); - expect(host.copy('common.refresh')).toBe('Refresh'); - expect(host.copy('game.host.start_round')).toBe('Start round'); - expect(player.copy('game.player.submit_guess')).toBe('Submit guess'); - - setPreferredLocale('da'); - - expect(host.copy('common.refresh')).toBe('Opdatér'); - expect(host.copy('game.host.start_round')).toBe('Start runde'); - expect(player.copy('game.player.submit_guess')).toBe('Send gæt'); + expect(host.copy('common.refresh')).toBe(hostRefresh); + expect(host.copy('game.host.start_round')).toBe(hostStartRound); + expect(player.copy('game.player.submit_guess')).toBe(playerSubmitGuess); player.ngOnDestroy(); host.ngOnDestroy(); }); - it('keeps audio routing policy primary-only (client has no audio output)', async () => { - const originalPlay = vi.fn().mockRejectedValue(new Error('original play')); + it('keeps audio routing primary-only by guarding player playback without muting the host path', async () => { + const originalPlay = vi.fn().mockRejectedValue(new Error('primary host playback')); const mediaPrototype = { play: originalPlay }; vi.stubGlobal('window', { @@ -59,7 +71,7 @@ describe('i18n MVP flow smoke (host/player + audio policy)', () => { const host = new HostShellComponent(); host.ngOnInit(); - await expect(mediaPrototype.play()).rejects.toThrow('original play'); + await expect(mediaPrototype.play()).rejects.toThrow('primary host playback'); const player = new PlayerShellComponent(); player.ngOnInit(); @@ -68,7 +80,7 @@ describe('i18n MVP flow smoke (host/player + audio policy)', () => { player.ngOnDestroy(); - await expect(mediaPrototype.play()).rejects.toThrow('original play'); + await expect(mediaPrototype.play()).rejects.toThrow('primary host playback'); host.ngOnDestroy(); }); -- 2.39.5