Files
Portfolio/tests/browser/audio-controls.spec.ts

96 lines
4.9 KiB
TypeScript

import { expect, test } from '../helpers/serve-dist';
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const OriginalAudioContext = window.AudioContext;
(window as unknown as { __audioConstructed: number }).__audioConstructed = 0;
(window as unknown as { __audioContexts: AudioContext[] }).__audioContexts = [];
if (!OriginalAudioContext) return;
class TrackingAudioContext extends OriginalAudioContext {
constructor(options?: AudioContextOptions) {
super(options);
const state = window as unknown as { __audioConstructed: number; __audioContexts: AudioContext[] };
state.__audioConstructed += 1;
state.__audioContexts.push(this);
}
}
Object.defineProperty(window, 'AudioContext', { configurable: true, value: TrackingAudioContext });
});
});
test('starts Web Audio synchronously from the visitor gesture and exposes accessible controls', async ({ page, distUrl }) => {
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await page.evaluate(() => {
(window as unknown as { __constructedDuringGesture?: number }).__constructedDuringGesture = -1;
window.addEventListener('click', (event) => {
if ((event.target as HTMLElement | null)?.textContent?.includes('START THE DRIVE')) {
const state = window as unknown as { __audioConstructed: number; __constructedDuringGesture: number };
state.__constructedDuringGesture = state.__audioConstructed;
}
});
});
await expect(page.getByRole('button', { name: 'START THE DRIVE' })).toBeVisible();
await expect(page.getByText('Enables the interactive soundtrack.')).toBeVisible();
await expect(page.getByLabel('Sound volume')).toHaveCount(0);
await expect.poll(() => page.evaluate(() => (window as unknown as { __audioConstructed: number }).__audioConstructed)).toBe(0);
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
await expect(page.getByRole('button', { name: 'Mute sound' })).toBeVisible();
await expect(page.getByLabel('Sound volume')).toBeVisible();
await expect.poll(() => page.evaluate(() => (window as unknown as { __constructedDuringGesture: number }).__constructedDuringGesture)).toBe(1);
await expect.poll(() => page.evaluate(() => (window as unknown as { __audioConstructed: number }).__audioConstructed)).toBe(1);
await page.getByRole('button', { name: 'Mute sound' }).click();
await expect(page.getByRole('button', { name: 'Unmute sound' })).toBeVisible();
});
test('persists native volume and keeps sound independent from Static motion', async ({ page, distUrl }) => {
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
const volume = page.getByLabel('Sound volume');
await volume.fill('0.65');
await expect(volume).toHaveValue('0.65');
await expect.poll(() => page.evaluate(() => localStorage.getItem('portfolio.audio.volume'))).toBe('0.65');
await page.getByLabel('Quality control').selectOption('static');
await expect(page.getByRole('button', { name: 'Unmute sound' })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Mute sound' })).toBeVisible();
await expect(volume).toBeVisible();
});
test('requires a direct visitor action after browser interruption without creating another graph', async ({ page, distUrl }) => {
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
await page.evaluate(() => {
const context = (window as unknown as { __audioContexts: AudioContext[] }).__audioContexts[0];
Object.defineProperty(context, 'state', { configurable: true, value: 'interrupted' });
context?.dispatchEvent(new Event('statechange'));
});
await expect(page.getByRole('button', { name: 'Resume sound' })).toBeVisible();
await page.getByRole('button', { name: 'Resume sound' }).click();
await expect(page.getByRole('button', { name: 'Mute sound' })).toBeVisible();
await expect.poll(() => page.evaluate(() => (window as unknown as { __audioConstructed: number }).__audioConstructed)).toBe(1);
});
test('shows a concise non-blocking message when initial audio start is rejected', async ({ page, distUrl }) => {
await page.addInitScript(() => {
const OriginalAudioContext = window.AudioContext;
if (!OriginalAudioContext) return;
class RejectingAudioContext extends OriginalAudioContext {
override resume(): Promise<void> { return Promise.reject(new DOMException('blocked', 'NotAllowedError')); }
}
Object.defineProperty(window, 'AudioContext', { configurable: true, value: RejectingAudioContext });
});
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
await expect(page.getByText('Audio unavailable. Continue without sound.')).toBeVisible();
await expect(page.locator('main#portfolio-content')).toBeVisible();
});