feat: audio playback intermittent issue
Task #50 completed by ClaudeQueue ClaudeQueue
This commit is contained in:
72
tests/audio-watchdog.smoke.spec.js
Normal file
72
tests/audio-watchdog.smoke.spec.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Smoke test for the background-audio-resume watchdog (Task #50).
|
||||
*
|
||||
* iOS Safari — most visibly in PWA/standalone mode on iPhone — can silently
|
||||
* pause a background <audio> element (screen lock, app switch) without the
|
||||
* app reacting, while a muted foreground <video> element keeps rolling. This
|
||||
* doesn't try to simulate real OS audio suspension (Playwright/Chromium
|
||||
* can't), but it does verify:
|
||||
* 1. The new visibilitychange/pageshow/focus listeners don't throw and are
|
||||
* wired up without breaking page load.
|
||||
* 2. Player.resumeIfNeeded() actually resumes a sounding element that is
|
||||
* paused while intent (_wantsPlaying) is true, and leaves it alone when
|
||||
* intent is false (a real user pause).
|
||||
*/
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Background audio resume watchdog', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('.app', { state: 'attached' });
|
||||
});
|
||||
|
||||
test('page loads with no JS errors and the new listeners are wired', async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
|
||||
await page.evaluate(() => {
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
window.dispatchEvent(new Event('pageshow'));
|
||||
window.dispatchEvent(new Event('focus'));
|
||||
});
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('resumeIfNeeded() resumes a silently-paused sounding element while intent is true', async ({ page }) => {
|
||||
const result = await page.evaluate(async () => {
|
||||
const audio = document.getElementById('audio');
|
||||
// Simulate a loaded audio-only track without going through the native
|
||||
// bridge (not available in this headless smoke context).
|
||||
audio.src = 'data:audio/mpeg;base64,//uQxAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAACAAAJmwAeHh4eHh4eHh4eLi4uLi4uLi4uLi5AQEBAQEBAQEBAQFJSUlJSUlJSUlJSZGRkZGRkZGRkZGR2dnZ2dnZ2dnZ2dg==';
|
||||
Player.mode = 'audio';
|
||||
Player.master = audio;
|
||||
Player.secondary = null;
|
||||
Player._wantsPlaying = true;
|
||||
audio.pause(); // simulate iOS silently pausing the sounding element
|
||||
const pausedBefore = audio.paused;
|
||||
Player.resumeIfNeeded();
|
||||
// play() is async; give the browser a tick to apply it.
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
return { pausedBefore, pausedAfter: audio.paused };
|
||||
});
|
||||
expect(result.pausedBefore).toBe(true);
|
||||
expect(result.pausedAfter).toBe(false);
|
||||
});
|
||||
|
||||
test('resumeIfNeeded() leaves a real user pause alone', async ({ page }) => {
|
||||
const result = await page.evaluate(async () => {
|
||||
const audio = document.getElementById('audio');
|
||||
audio.src = 'data:audio/mpeg;base64,//uQxAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAACAAAJmwAeHh4eHh4eHh4eLi4uLi4uLi4uLi5AQEBAQEBAQEBAQFJSUlJSUlJSUlJSZGRkZGRkZGRkZGR2dnZ2dnZ2dnZ2dg==';
|
||||
Player.mode = 'audio';
|
||||
Player.master = audio;
|
||||
Player.secondary = null;
|
||||
Player._wantsPlaying = false; // user explicitly paused
|
||||
audio.pause();
|
||||
Player.resumeIfNeeded();
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
return { paused: audio.paused };
|
||||
});
|
||||
expect(result.paused).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user