348 lines
No EOL
12 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
}); |