import { test, expect, Page } from '@playwright/test'; import * as path from 'path'; // Test data const testEmail = `user+stream-${Date.now()}@lab.veza`; const testPassword = `V3za!stream-${Date.now()}`; const audioFile = path.join(__dirname, '../../data/audio/demo-track-10s.mp3'); // Helper to register and login async function registerAndLogin(page: Page) { await page.goto('/register'); await page.fill('input[type="email"]', testEmail); await page.fill('input[type="password"]', testPassword); const confirmField = page.locator('input[name="confirmPassword"]'); if (await confirmField.isVisible()) { await confirmField.fill(testPassword); } await page.locator('button[type="submit"]').click(); await expect(page).toHaveURL(/\/(dashboard|home|app|stream)/, { timeout: 10000 }); } test.describe('Streaming Functionality', () => { test.beforeEach(async ({ page }) => { // Register and login await registerAndLogin(page); }); test('should load streaming interface', async ({ page }) => { await page.goto('/stream'); // Check main streaming elements await expect(page.locator('[data-testid="stream-container"], .stream-container, #stream')).toBeVisible(); // Check for player controls const player = page.locator('audio, video, [data-testid="media-player"]'); await expect(player).toBeVisible(); // Check for upload button await expect(page.locator('button:has-text("Upload"), [data-testid="upload-audio"]')).toBeVisible(); }); test('should upload audio file', async ({ page }) => { await page.goto('/stream'); // Find upload button const uploadButton = page.locator('input[type="file"][accept*="audio"], [data-testid="audio-upload-input"]'); if (!await uploadButton.isVisible()) { // Click button to reveal file input await page.locator('button:has-text("Upload")').click(); } // Upload file await uploadButton.setInputFiles(audioFile); // Wait for upload to complete await expect(page.locator('text=/upload.*complete|success|uploaded/i')).toBeVisible({ timeout: 30000 }); // File should appear in list or player await expect(page.locator('text=demo-track-10s.mp3')).toBeVisible(); }); test('should play uploaded audio', async ({ page }) => { await page.goto('/stream'); // Upload a file first const uploadButton = page.locator('input[type="file"][accept*="audio"]'); await uploadButton.setInputFiles(audioFile); await page.waitForTimeout(2000); // Wait for upload // Find play button const playButton = page.locator('button[aria-label="Play"], [data-testid="play-button"], .play-button'); if (await playButton.isVisible()) { await playButton.click(); // Check if audio is playing const isPlaying = await page.evaluate(() => { const audio = document.querySelector('audio') as HTMLAudioElement; return audio && !audio.paused; }); expect(isPlaying).toBeTruthy(); // Pause button should now be visible await expect(page.locator('button[aria-label="Pause"], [data-testid="pause-button"]')).toBeVisible(); } }); test('should show audio waveform or visualizer', async ({ page }) => { await page.goto('/stream'); // Upload file const uploadButton = page.locator('input[type="file"][accept*="audio"]'); await uploadButton.setInputFiles(audioFile); await page.waitForTimeout(2000); // Check for waveform/visualizer const visualizer = page.locator('canvas[class*="waveform"], canvas[class*="visualizer"], .audio-waveform, svg.waveform'); if (await visualizer.isVisible({ timeout: 5000 }).catch(() => false)) { // Visualizer should be rendered const box = await visualizer.boundingBox(); expect(box?.width).toBeGreaterThan(0); expect(box?.height).toBeGreaterThan(0); } }); test('should handle volume control', async ({ page }) => { await page.goto('/stream'); // Find volume control const volumeSlider = page.locator('input[type="range"][aria-label*="Volume"], [data-testid="volume-slider"]'); if (await volumeSlider.isVisible()) { // Get initial volume const initialVolume = await volumeSlider.inputValue(); // Change volume await volumeSlider.fill('50'); // Verify volume changed const audio = page.locator('audio'); const audioVolume = await audio.evaluate((el: HTMLAudioElement) => el.volume * 100); expect(audioVolume).toBeCloseTo(50, 1); } }); test('should show playback progress', async ({ page }) => { await page.goto('/stream'); // Upload and play file const uploadButton = page.locator('input[type="file"][accept*="audio"]'); await uploadButton.setInputFiles(audioFile); await page.waitForTimeout(2000); // Play audio await page.locator('button[aria-label="Play"], [data-testid="play-button"]').click(); // Check progress bar const progressBar = page.locator('input[type="range"][aria-label*="Seek"], [data-testid="progress-bar"], .progress-bar'); if (await progressBar.isVisible()) { // Wait a bit for playback await page.waitForTimeout(2000); // Progress should have moved const progress = await progressBar.inputValue(); expect(parseInt(progress)).toBeGreaterThan(0); } // Check time display const timeDisplay = page.locator('.current-time, [data-testid="current-time"]'); if (await timeDisplay.isVisible()) { const time = await timeDisplay.textContent(); expect(time).not.toBe('0:00'); } }); test('should handle seek functionality', async ({ page }) => { await page.goto('/stream'); // Upload and play file const uploadButton = page.locator('input[type="file"][accept*="audio"]'); await uploadButton.setInputFiles(audioFile); await page.waitForTimeout(2000); // Find seek bar const seekBar = page.locator('input[type="range"][aria-label*="Seek"], [data-testid="seek-bar"]'); if (await seekBar.isVisible()) { // Seek to middle await seekBar.fill('50'); // Verify audio currentTime changed const audio = page.locator('audio'); const currentTime = await audio.evaluate((el: HTMLAudioElement) => el.currentTime); const duration = await audio.evaluate((el: HTMLAudioElement) => el.duration); expect(currentTime).toBeCloseTo(duration * 0.5, 1); } }); test('should show playlist or track list', async ({ page }) => { await page.goto('/stream'); // Check for playlist const playlist = page.locator('[data-testid="playlist"], .playlist, .track-list'); if (await playlist.isVisible()) { // Upload multiple files for (let i = 0; i < 3; i++) { const uploadButton = page.locator('input[type="file"][accept*="audio"]'); await uploadButton.setInputFiles(audioFile); await page.waitForTimeout(1000); } // Should show multiple tracks const tracks = await playlist.locator('.track-item, [data-testid^="track-"]').count(); expect(tracks).toBeGreaterThanOrEqual(3); } }); test('should handle playback controls', async ({ page }) => { await page.goto('/stream'); // Upload file const uploadButton = page.locator('input[type="file"][accept*="audio"]'); await uploadButton.setInputFiles(audioFile); await page.waitForTimeout(2000); // Test play/pause const playButton = page.locator('button[aria-label="Play"]'); await playButton.click(); const pauseButton = page.locator('button[aria-label="Pause"]'); await expect(pauseButton).toBeVisible(); await pauseButton.click(); await expect(playButton).toBeVisible(); // Test skip controls if available const nextButton = page.locator('button[aria-label="Next"], [data-testid="next-track"]'); const prevButton = page.locator('button[aria-label="Previous"], [data-testid="prev-track"]'); if (await nextButton.isVisible()) { await nextButton.click(); // Should move to next track or end } if (await prevButton.isVisible()) { await prevButton.click(); // Should move to previous track or beginning } }); test('should handle repeat and shuffle', async ({ page }) => { await page.goto('/stream'); // Check for repeat button const repeatButton = page.locator('button[aria-label*="Repeat"], [data-testid="repeat-button"]'); if (await repeatButton.isVisible()) { await repeatButton.click(); // Should show active state await expect(repeatButton).toHaveClass(/active|on/); } // Check for shuffle button const shuffleButton = page.locator('button[aria-label*="Shuffle"], [data-testid="shuffle-button"]'); if (await shuffleButton.isVisible()) { await shuffleButton.click(); // Should show active state await expect(shuffleButton).toHaveClass(/active|on/); } }); test('should show metadata for playing track', async ({ page }) => { await page.goto('/stream'); // Upload file const uploadButton = page.locator('input[type="file"][accept*="audio"]'); await uploadButton.setInputFiles(audioFile); await page.waitForTimeout(2000); // Check for metadata display await expect(page.locator('text=demo-track-10s')).toBeVisible(); // Check for duration const duration = page.locator('.duration, [data-testid="track-duration"]'); if (await duration.isVisible()) { const durationText = await duration.textContent(); expect(durationText).toMatch(/\d+:\d+/); } }); test('should handle streaming errors gracefully', async ({ page }) => { await page.goto('/stream'); // Try to play non-existent track await page.evaluate(() => { const audio = document.querySelector('audio') as HTMLAudioElement; if (audio) { audio.src = 'https://invalid-url/non-existent.mp3'; audio.play().catch(() => {}); } }); // Should show error message await expect(page.locator('text=/error|failed|cannot.*play/i')).toBeVisible({ timeout: 5000 }); }); test('should support keyboard controls', async ({ page }) => { await page.goto('/stream'); // Upload file const uploadButton = page.locator('input[type="file"][accept*="audio"]'); await uploadButton.setInputFiles(audioFile); await page.waitForTimeout(2000); // Focus player await page.locator('[data-testid="media-player"], audio').focus(); // Test spacebar for play/pause await page.keyboard.press('Space'); // Check if playing const isPlaying = await page.evaluate(() => { const audio = document.querySelector('audio') as HTMLAudioElement; return audio && !audio.paused; }); expect(isPlaying).toBeTruthy(); // Test arrow keys for seek await page.keyboard.press('ArrowRight'); await page.waitForTimeout(500); const currentTime = await page.evaluate(() => { const audio = document.querySelector('audio') as HTMLAudioElement; return audio?.currentTime || 0; }); expect(currentTime).toBeGreaterThan(0); }); test('should remember volume preference', async ({ page }) => { await page.goto('/stream'); // Set volume const volumeSlider = page.locator('input[type="range"][aria-label*="Volume"]'); if (await volumeSlider.isVisible()) { await volumeSlider.fill('30'); // Reload page await page.reload(); // Volume should be remembered const savedVolume = await volumeSlider.inputValue(); expect(parseInt(savedVolume)).toBe(30); } }); });