test: add issue 278 locale and audio smoke gate
This commit is contained in:
36
docs/ISSUE-278-SMOKE-E2E-GATE-ARTIFACT.md
Normal file
36
docs/ISSUE-278-SMOKE-E2E-GATE-ARTIFACT.md
Normal file
@@ -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.
|
||||||
@@ -4,44 +4,56 @@ import { HostShellComponent } from './features/host/host-shell.component';
|
|||||||
import { PlayerShellComponent } from './features/player/player-shell.component';
|
import { PlayerShellComponent } from './features/player/player-shell.component';
|
||||||
import { setPreferredLocale } from './lobby-i18n';
|
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)', () => {
|
describe('i18n MVP flow smoke (host/player + audio policy)', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves host/player copy in en and da from shared catalog', () => {
|
it.each([
|
||||||
vi.stubGlobal('window', {
|
{
|
||||||
location: { hash: '', search: '' },
|
locale: 'en',
|
||||||
history: { state: null, replaceState: vi.fn() },
|
hostRefresh: 'Refresh',
|
||||||
localStorage: { getItem: vi.fn().mockReturnValue('en'), setItem: vi.fn(), removeItem: vi.fn() },
|
hostStartRound: 'Start round',
|
||||||
sessionStorage: { getItem: vi.fn().mockReturnValue(null), setItem: vi.fn(), removeItem: vi.fn() },
|
playerSubmitGuess: 'Submit guess',
|
||||||
addEventListener: vi.fn(),
|
},
|
||||||
removeEventListener: vi.fn(),
|
{
|
||||||
});
|
locale: 'da',
|
||||||
vi.stubGlobal('navigator', { language: 'en-US', onLine: true });
|
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 host = new HostShellComponent();
|
||||||
const player = new PlayerShellComponent();
|
const player = new PlayerShellComponent();
|
||||||
host.ngOnInit();
|
host.ngOnInit();
|
||||||
player.ngOnInit();
|
player.ngOnInit();
|
||||||
|
setPreferredLocale(locale);
|
||||||
|
|
||||||
expect(host.copy('common.refresh')).toBe('Refresh');
|
expect(host.copy('common.refresh')).toBe(hostRefresh);
|
||||||
expect(host.copy('game.host.start_round')).toBe('Start round');
|
expect(host.copy('game.host.start_round')).toBe(hostStartRound);
|
||||||
expect(player.copy('game.player.submit_guess')).toBe('Submit guess');
|
expect(player.copy('game.player.submit_guess')).toBe(playerSubmitGuess);
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
player.ngOnDestroy();
|
player.ngOnDestroy();
|
||||||
host.ngOnDestroy();
|
host.ngOnDestroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps audio routing policy primary-only (client has no audio output)', async () => {
|
it('keeps audio routing primary-only by guarding player playback without muting the host path', async () => {
|
||||||
const originalPlay = vi.fn().mockRejectedValue(new Error('original play'));
|
const originalPlay = vi.fn().mockRejectedValue(new Error('primary host playback'));
|
||||||
const mediaPrototype = { play: originalPlay };
|
const mediaPrototype = { play: originalPlay };
|
||||||
|
|
||||||
vi.stubGlobal('window', {
|
vi.stubGlobal('window', {
|
||||||
@@ -59,7 +71,7 @@ describe('i18n MVP flow smoke (host/player + audio policy)', () => {
|
|||||||
const host = new HostShellComponent();
|
const host = new HostShellComponent();
|
||||||
host.ngOnInit();
|
host.ngOnInit();
|
||||||
|
|
||||||
await expect(mediaPrototype.play()).rejects.toThrow('original play');
|
await expect(mediaPrototype.play()).rejects.toThrow('primary host playback');
|
||||||
|
|
||||||
const player = new PlayerShellComponent();
|
const player = new PlayerShellComponent();
|
||||||
player.ngOnInit();
|
player.ngOnInit();
|
||||||
@@ -68,7 +80,7 @@ describe('i18n MVP flow smoke (host/player + audio policy)', () => {
|
|||||||
|
|
||||||
player.ngOnDestroy();
|
player.ngOnDestroy();
|
||||||
|
|
||||||
await expect(mediaPrototype.play()).rejects.toThrow('original play');
|
await expect(mediaPrototype.play()).rejects.toThrow('primary host playback');
|
||||||
|
|
||||||
host.ngOnDestroy();
|
host.ngOnDestroy();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user