S1 fix real: deblocare audio pe primul gest (acopera resume), nu doar btn-start

Fix-ul initial deblocà AudioContext-ul doar in handlerul btn-start. Lacuna:
calea de resume (reload mid-campanie) intra direct pe harta fara btn-start ->
ctx nedeblocat -> camere mute. Plus resume() singur nu ajunge pe iOS Safari.

- unlockAudio() + listener global one-time (pointerdown+keydown capture):
  acopera fresh SI resume; buffer silentios iOS-safe.
- beep() se auto-vindeca daca ctx redevine suspended.
- Test smoke #9 rescris: headless creeaza ctx direct 'running' (ignora autoplay)
  -> vechiul "ctx running" trecea trivial. Acum: gest tastatura fara btn-start
  -> running (cale resume) + beep self-heal din ctx suspendat.
- Demo campanie regenerat. Suita 24/24.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-13 11:29:21 +00:00
parent 27fc0ca901
commit 651025bd28
4 changed files with 89 additions and 21 deletions

View File

@@ -945,27 +945,48 @@ test.describe('Campanie E2E @campanie', () => {
});
// ─────────────────────────────────────────────────────────────────────
// Test 9 (S4): Audio — AudioContext deblocat la "Incepe aventura" (S1)
// Test 9 (S4+): Audio — deblocare ctx pe PRIMUL gest (orice), nu doar btn-start.
// NB: headless Chromium creeaza AudioContext direct 'running' (ignora autoplay
// policy), deci "ctx==running" e trivial. Testam WIRING-ul real al deblocarii:
// (A) primul gest oarecare (tastatura, ca la resume) deblocheaza fara btn-start;
// (B) beep() se auto-vindeca dintr-un ctx readus in 'suspended'.
// ─────────────────────────────────────────────────────────────────────
test('audio — AudioContext deblocat la Incepe aventura (S1) @campanie',
test('audio — deblocare pe primul gest + beep self-heal (S1) @campanie',
async ({ page }) => {
const cfg = campaignCfg(3, 'classic');
const tmpPath = await writeCampaignHtml(page, cfg, 'audio');
const gp = await page.context().newPage();
try {
await gp.goto('file://' + tmpPath);
// Inainte de gest: ctx inexistent (creat lazy)
// Inainte de orice gest: ctx inexistent (creat lazy)
const before = await gp.evaluate(
() => (window.beep && window.beep._ctx) ? window.beep._ctx.state : 'NO_CTX'
);
expect(before, 'ctx nu trebuie sa existe inainte de gest').toBe('NO_CTX');
// Gestul pe parinte deblocheaza ctx-ul
await gp.locator('#btn-start').click();
await gp.waitForTimeout(200);
const after = await gp.evaluate(
// (A) Cale RESUME: un gest de tastatura (mers pe harta), FARA btn-start,
// trebuie sa creeze+deblocheze ctx-ul prin listenerul global one-time.
await gp.keyboard.press('ArrowDown');
await gp.waitForTimeout(100);
const afterKey = await gp.evaluate(
() => (window.beep && window.beep._ctx) ? window.beep._ctx.state : 'NO_CTX'
);
expect(after, 'ctx trebuie running dupa Incepe aventura').toBe('running');
expect(afterKey, 'gestul de tastatura trebuie sa deblocheze ctx (cale resume)').toBe('running');
// (B) beep() trebuie sa produca oscilatoare si sa reia un ctx suspendat.
const heal = await gp.evaluate(async () => {
await window.beep._ctx.suspend();
const mid = window.beep._ctx.state; // 'suspended'
let osc = 0;
const orig = window.beep._ctx.createOscillator.bind(window.beep._ctx);
window.beep._ctx.createOscillator = function () { osc++; return orig(); };
window.beep(true);
await new Promise(r => setTimeout(r, 50));
return { mid, osc, end: window.beep._ctx.state };
});
expect(heal.mid, 'ctx trebuie suspendat inainte de self-heal').toBe('suspended');
expect(heal.osc, 'beep trebuie sa creeze oscilatoare').toBeGreaterThan(0);
expect(heal.end, 'beep trebuie sa reia ctx-ul suspendat').toBe('running');
} finally {
await gp.close();
try { unlinkSync(tmpPath); } catch (_) {}