diff --git a/apps/web/e2e/upload-review.spec.ts b/apps/web/e2e/upload-review.spec.ts index 85afc2b..713401e 100644 --- a/apps/web/e2e/upload-review.spec.ts +++ b/apps/web/e2e/upload-review.spec.ts @@ -1,7 +1,7 @@ import { expect, test, type Page } from '@playwright/test'; -function previewWav() { - const dataSize = 8000 * 2 * 2; +function previewWav(seconds = 2) { + const dataSize = 8000 * 2 * seconds; const wav = Buffer.alloc(44 + dataSize); wav.write('RIFF', 0); wav.writeUInt32LE(36 + dataSize, 4); wav.write('WAVEfmt ', 8); wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22); @@ -216,3 +216,24 @@ test('queue preview explains HTTP failures and recovers on retry', async ({ page await expect.poll(() => preview.locator('audio').evaluate((el: HTMLAudioElement) => el.duration)).toBe(2); await expect(preview.getByRole('alert')).toHaveCount(0); }); + +test('starting another queue preview pauses the previous track', async ({ page }) => { + const wav = previewWav(10); + await page.route('**/api/v1/admin/uploads/files/*/stream?*', (route) => route.fulfill({ contentType: 'audio/wav', body: wav })); + await page.route('**/api/v1/stream/mobile-preview', (route) => route.fulfill({ contentType: 'audio/wav', body: wav })); + await setup(page, 'queue'); + await page.getByRole('button', { name: 'Развернуть пачку', exact: true }).click(); + const first = page.locator('audio').nth(0); + const second = page.locator('audio').nth(1); + await first.evaluate((el: HTMLAudioElement) => el.play()); + await expect.poll(() => first.evaluate((el: HTMLAudioElement) => el.currentTime)).toBeGreaterThan(0); + await second.evaluate((el: HTMLAudioElement) => el.play()); + await expect.poll(() => first.evaluate((el: HTMLAudioElement) => el.paused)).toBe(true); + expect(await second.evaluate((el: HTMLAudioElement) => el.paused)).toBe(false); + expect(await first.evaluate((el: HTMLAudioElement) => el.currentTime)).toBeGreaterThan(0); + await first.evaluate((el: HTMLAudioElement) => el.play()); + await expect.poll(() => second.evaluate((el: HTMLAudioElement) => el.paused)).toBe(true); + expect(await first.evaluate((el: HTMLAudioElement) => el.paused)).toBe(false); + await page.getByRole('button', { name: 'Свернуть пачку', exact: true }).click(); + await expect.poll(() => first.evaluate((el: HTMLAudioElement) => el.paused)).toBe(true); +}); diff --git a/apps/web/src/features/uploads/AudioPreview.tsx b/apps/web/src/features/uploads/AudioPreview.tsx index c77c0e9..9352c98 100644 --- a/apps/web/src/features/uploads/AudioPreview.tsx +++ b/apps/web/src/features/uploads/AudioPreview.tsx @@ -5,6 +5,9 @@ import { refreshIcon } from '../../shared/ui/icons'; import { Button } from '../../shared/ui/ui'; import u from '../../pages/uploads/uploads.module.css'; +// Preview elements may belong to different batches, but only one may play. +const previews = new Set(); + export function AudioPreview({ url, name, active }: { url: string; name: string; active: boolean }) { const { t } = useTranslation('admin'); const queryClient = useQueryClient(); @@ -12,6 +15,16 @@ export function AudioPreview({ url, name, active }: { url: string; name: string; const [error, setError] = useState(null); const [httpStatus, setHttpStatus] = useState(null); + useEffect(() => { + const element = audio.current; + if (!element) return; + previews.add(element); + return () => { previews.delete(element); element.pause(); }; + }, []); + useEffect(() => { + if (!active) audio.current?.pause(); + }, [active]); + useEffect(() => { setError(null); setHttpStatus(null); }, [url]); useEffect(() => { if (error === null) return; @@ -37,6 +50,12 @@ export function AudioPreview({ url, name, active }: { url: string; name: string; return