veza/tools/tests/e2e/specs/stream.spec.ts
2025-12-03 22:56:50 +01:00

348 lines
No EOL
12 KiB
TypeScript

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);
}
});
});