/** * E2E tests for queue interactions * Tests queue management user flows, download control, and persistence */ import { test, expect } from '@playwright/test'; test.describe('Queue Page - Initial Load', () => { test('should load queue page successfully', async ({ page }) => { await page.goto('/queue'); await expect(page).toHaveTitle(/Queue|Downloads/i); await expect(page.locator('h1, h2')).toContainText(/Queue|Downloads/i); }); test('should display queue statistics', async ({ page }) => { await page.goto('/queue'); await expect(page.locator('#pending-count')).toBeVisible(); await expect(page.locator('#active-count')).toBeVisible(); await expect(page.locator('#completed-count')).toBeVisible(); await expect(page.locator('#failed-count')).toBeVisible(); }); test('should display queue control buttons', async ({ page }) => { await page.goto('/queue'); await expect(page.locator('#start-queue-btn')).toBeVisible(); await expect(page.locator('#stop-queue-btn')).toBeVisible(); await expect(page.locator('#clear-completed-btn')).toBeVisible(); await expect(page.locator('#clear-failed-btn')).toBeVisible(); await expect(page.locator('#retry-all-btn')).toBeVisible(); }); test('should display queue sections', async ({ page }) => { await page.goto('/queue'); await expect(page.locator('#pending-queue')).toBeVisible(); await expect(page.locator('#active-downloads')).toBeVisible(); await expect(page.locator('#completed-queue')).toBeVisible(); await expect(page.locator('#failed-queue')).toBeVisible(); }); }); test.describe('Queue Controls - Start/Stop', () => { test('should start queue on button click', async ({ page }) => { await page.goto('/queue'); // Mock API response await page.route('/api/queue/start', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ message: 'Queue started' }) }); }); await page.click('#start-queue-btn'); // Verify toast or visual feedback await expect(page.locator('.toast, .notification')).toContainText(/started/i); }); test('should stop queue on button click', async ({ page }) => { await page.goto('/queue'); // Mock API response await page.route('/api/queue/stop', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ message: 'Queue stopped' }) }); }); await page.click('#stop-queue-btn'); // Verify toast or visual feedback await expect(page.locator('.toast, .notification')).toContainText(/stopped/i); }); test('should disable start button when queue is running', async ({ page }) => { await page.goto('/queue'); // Mock queue status response await page.route('/api/queue/status', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ status: { is_running: true, pending_items: [], active_downloads: [], completed_items: [], failed_items: [] }, statistics: { pending: 0, active: 0, completed: 0, failed: 0, total: 0 } }) }); }); await page.reload(); const startBtn = page.locator('#start-queue-btn'); await expect(startBtn).toBeDisabled(); }); test('should handle queue start error gracefully', async ({ page }) => { await page.goto('/queue'); // Mock error response await page.route('/api/queue/start', async route => { await route.fulfill({ status: 500, body: JSON.stringify({ error: 'Queue already running' }) }); }); await page.click('#start-queue-btn'); // Verify error message displayed await expect(page.locator('.toast, .error, .notification')).toContainText(/error|failed/i); }); }); test.describe('Queue Management - Clear Operations', () => { test('should show confirmation before clearing completed', async ({ page }) => { await page.goto('/queue'); await page.click('#clear-completed-btn'); // Verify confirmation modal appears const modal = page.locator('#confirm-modal, .modal'); await expect(modal).toBeVisible(); await expect(modal).toContainText(/clear.*completed/i); }); test('should clear completed downloads on confirmation', async ({ page }) => { await page.goto('/queue'); // Mock API response await page.route('/api/queue/completed', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ cleared: 5 }) }); }); await page.click('#clear-completed-btn'); // Confirm action await page.click('#confirm-ok, button:has-text("OK"), button:has-text("Yes")'); // Verify toast confirmation await expect(page.locator('.toast, .notification')).toContainText(/cleared|success/i); }); test('should cancel clear completed on modal cancel', async ({ page }) => { await page.goto('/queue'); let apiCalled = false; await page.route('/api/queue/completed', async route => { apiCalled = true; await route.fulfill({ status: 200, body: '{}' }); }); await page.click('#clear-completed-btn'); await page.click('#confirm-cancel, button:has-text("Cancel"), button:has-text("No")'); // Wait a bit to ensure API is not called await page.waitForTimeout(500); expect(apiCalled).toBe(false); }); test('should show confirmation before clearing failed', async ({ page }) => { await page.goto('/queue'); await page.click('#clear-failed-btn'); const modal = page.locator('#confirm-modal, .modal'); await expect(modal).toBeVisible(); await expect(modal).toContainText(/clear.*failed/i); }); test('should clear failed downloads on confirmation', async ({ page }) => { await page.goto('/queue'); await page.route('/api/queue/failed', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ cleared: 3 }) }); }); await page.click('#clear-failed-btn'); await page.click('#confirm-ok, button:has-text("OK")'); await expect(page.locator('.toast, .notification')).toContainText(/cleared|success/i); }); test('should show confirmation before clearing pending', async ({ page }) => { await page.goto('/queue'); await page.click('#clear-pending-btn'); const modal = page.locator('#confirm-modal, .modal'); await expect(modal).toBeVisible(); await expect(modal).toContainText(/pending|remove/i); }); test('should clear pending downloads on confirmation', async ({ page }) => { await page.goto('/queue'); await page.route('/api/queue/pending', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ cleared: 2 }) }); }); await page.click('#clear-pending-btn'); await page.click('#confirm-ok, button:has-text("OK")'); await expect(page.locator('.toast, .notification')).toContainText(/cleared|removed|success/i); }); }); test.describe('Queue Management - Retry Failed', () => { test('should show confirmation before retrying failed downloads', async ({ page }) => { await page.goto('/queue'); await page.click('#retry-all-btn'); const modal = page.locator('#confirm-modal, .modal'); await expect(modal).toBeVisible(); await expect(modal).toContainText(/retry/i); }); test('should retry all failed downloads on confirmation', async ({ page }) => { await page.goto('/queue'); // Mock initial queue status with failed items await page.route('/api/queue/status', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ status: { is_running: false, pending_items: [], active_downloads: [], completed_items: [], failed_items: [ { id: 'failed-1', serie_name: 'Anime 1', episode: 1 }, { id: 'failed-2', serie_name: 'Anime 2', episode: 2 } ] }, statistics: { pending: 0, active: 0, completed: 0, failed: 2, total: 2 } }) }); }); await page.route('/api/queue/retry', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ retried: 2 }) }); }); await page.reload(); await page.click('#retry-all-btn'); await page.click('#confirm-ok, button:has-text("OK")'); await expect(page.locator('.toast, .notification')).toContainText(/retry|success/i); }); test('should not retry if no failed downloads exist', async ({ page }) => { await page.goto('/queue'); // Mock queue with no failed items await page.route('/api/queue/status', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ status: { is_running: false, pending_items: [], active_downloads: [], completed_items: [], failed_items: [] }, statistics: { pending: 0, active: 0, completed: 0, failed: 0, total: 0 } }) }); }); await page.reload(); // Button should be disabled const retryBtn = page.locator('#retry-all-btn'); await expect(retryBtn).toBeDisabled(); }); }); test.describe('Queue Display - Real-time Updates', () => { test('should update statistics when queue changes', async ({ page }) => { await page.goto('/queue'); // Initial state await expect(page.locator('#pending-count')).toHaveText('0'); // Simulate queue update via WebSocket await page.evaluate(() => { document.getElementById('pending-count').textContent = '5'; document.getElementById('active-count').textContent = '1'; }); await expect(page.locator('#pending-count')).toHaveText('5'); await expect(page.locator('#active-count')).toHaveText('1'); }); test('should display pending queue items', async ({ page }) => { await page.goto('/queue'); // Mock queue with pending items await page.route('/api/queue/status', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ status: { is_running: false, pending_items: [ { id: '1', serie_name: 'Anime 1', episode: 1 }, { id: '2', serie_name: 'Anime 2', episode: 2 } ], active_downloads: [], completed_items: [], failed_items: [] }, statistics: { pending: 2, active: 0, completed: 0, failed: 0, total: 2 } }) }); }); await page.reload(); const pendingQueue = page.locator('#pending-queue'); await expect(pendingQueue.locator('.queue-item')).toHaveCount(2); }); test('should display active downloads with progress', async ({ page }) => { await page.goto('/queue'); // Mock queue with active download await page.route('/api/queue/status', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ status: { is_running: true, pending_items: [], active_downloads: [ { id: '1', serie_name: 'Anime 1', episode: 1, progress: 45 } ], completed_items: [], failed_items: [] }, statistics: { pending: 0, active: 1, completed: 0, failed: 0, total: 1 } }) }); }); await page.reload(); const activeQueue = page.locator('#active-downloads'); await expect(activeQueue.locator('.queue-item')).toHaveCount(1); await expect(activeQueue.locator('.progress-bar, .progress')).toBeVisible(); }); test('should update progress bar in real-time', async ({ page }) => { await page.goto('/queue'); // Simulate progress update await page.evaluate(() => { const progressBar = document.createElement('div'); progressBar.className = 'progress-bar'; const progressFill = document.createElement('div'); progressFill.className = 'progress-fill'; progressFill.style.width = '50%'; progressBar.appendChild(progressFill); document.getElementById('active-downloads').appendChild(progressBar); }); const progressFill = page.locator('.progress-fill'); await expect(progressFill).toHaveCSS('width', /50%|[0-9]+px/); }); }); test.describe('Queue Persistence', () => { test('should persist queue state across page refresh', async ({ page }) => { await page.goto('/queue'); // Mock queue state await page.route('/api/queue/status', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ status: { is_running: false, pending_items: [ { id: '1', serie_name: 'Persisted Anime', episode: 1 } ], active_downloads: [], completed_items: [], failed_items: [] }, statistics: { pending: 1, active: 0, completed: 0, failed: 0, total: 1 } }) }); }); await page.reload(); // Verify queue items persist await expect(page.locator('#pending-count')).toHaveText('1'); await expect(page.locator('#pending-queue')).toContainText('Persisted Anime'); }); test('should maintain queue statistics after navigation', async ({ page }) => { await page.goto('/queue'); // Set initial statistics await page.evaluate(() => { document.getElementById('pending-count').textContent = '3'; document.getElementById('completed-count').textContent = '7'; }); const initialPending = await page.locator('#pending-count').textContent(); const initialCompleted = await page.locator('#completed-count').textContent(); // Navigate away and back await page.goto('/'); await page.goto('/queue'); // Note: In real app, these would be loaded from API // This test verifies the structure exists await expect(page.locator('#pending-count')).toBeVisible(); await expect(page.locator('#completed-count')).toBeVisible(); }); }); test.describe('Queue Accessibility', () => { test('should have accessible button labels', async ({ page }) => { await page.goto('/queue'); await expect(page.locator('#start-queue-btn')).toHaveAccessibleName(); await expect(page.locator('#stop-queue-btn')).toHaveAccessibleName(); await expect(page.locator('#clear-completed-btn')).toHaveAccessibleName(); await expect(page.locator('#clear-failed-btn')).toHaveAccessibleName(); await expect(page.locator('#retry-all-btn')).toHaveAccessibleName(); }); test('should support keyboard navigation for buttons', async ({ page }) => { await page.goto('/queue'); const startBtn = page.locator('#start-queue-btn'); await startBtn.focus(); await expect(startBtn).toBeFocused(); await page.keyboard.press('Tab'); const stopBtn = page.locator('#stop-queue-btn'); await expect(stopBtn).toBeFocused(); }); test('should support Enter key for button activation', async ({ page }) => { await page.goto('/queue'); let apiCalled = false; await page.route('/api/queue/start', async route => { apiCalled = true; await route.fulfill({ status: 200, body: JSON.stringify({ message: 'Started' }) }); }); await page.locator('#start-queue-btn').focus(); await page.keyboard.press('Enter'); await page.waitForTimeout(500); expect(apiCalled).toBe(true); }); test('should have ARIA labels for statistics', async ({ page }) => { await page.goto('/queue'); // Verify statistics have accessible context await expect(page.locator('#pending-count').locator('..')).toContainText(/pending/i); await expect(page.locator('#active-count').locator('..')).toContainText(/active/i); await expect(page.locator('#completed-count').locator('..')).toContainText(/completed/i); await expect(page.locator('#failed-count').locator('..')).toContainText(/failed/i); }); }); test.describe('Queue Edge Cases', () => { test('should handle empty queue gracefully', async ({ page }) => { await page.goto('/queue'); await page.route('/api/queue/status', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ status: { is_running: false, pending_items: [], active_downloads: [], completed_items: [], failed_items: [] }, statistics: { pending: 0, active: 0, completed: 0, failed: 0, total: 0 } }) }); }); await page.reload(); await expect(page.locator('#pending-count')).toHaveText('0'); await expect(page.locator('#total-count')).toHaveText('0'); }); test('should handle API error gracefully', async ({ page }) => { await page.goto('/queue'); await page.route('/api/queue/status', async route => { await route.fulfill({ status: 500, body: JSON.stringify({ error: 'Internal server error' }) }); }); await page.reload(); // Should display error message or default state await expect(page.locator('.error, .toast, .notification, body')).toBeVisible(); }); test('should handle rapid button clicks gracefully', async ({ page }) => { await page.goto('/queue'); let apiCallCount = 0; await page.route('/api/queue/start', async route => { apiCallCount++; await route.fulfill({ status: 200, body: JSON.stringify({ message: 'Started' }) }); }); // Click button multiple times rapidly const startBtn = page.locator('#start-queue-btn'); await startBtn.click(); await startBtn.click(); await startBtn.click(); await page.waitForTimeout(500); // Should not call API multiple times (debouncing/button state management) // This depends on implementation - may be 1 or 3 expect(apiCallCount).toBeGreaterThan(0); }); test('should handle very long queue items list', async ({ page }) => { await page.goto('/queue'); const longItemsList = Array.from({ length: 100 }, (_, i) => ({ id: `item-${i}`, serie_name: `Anime ${i}`, episode: i + 1 })); await page.route('/api/queue/status', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ status: { is_running: false, pending_items: longItemsList, active_downloads: [], completed_items: [], failed_items: [] }, statistics: { pending: 100, active: 0, completed: 0, failed: 0, total: 100 } }) }); }); await page.reload(); await expect(page.locator('#pending-count')).toHaveText('100'); // Should handle scrolling for long lists await expect(page.locator('#pending-queue')).toBeVisible(); }); }); test.describe('Queue Theme Integration', () => { test('should respect current theme', async ({ page }) => { await page.goto('/queue'); // Check that theme toggle exists const themeToggle = page.locator('#theme-toggle'); await expect(themeToggle).toBeVisible(); }); test('should apply theme to queue elements', async ({ page }) => { await page.goto('/queue'); // Get initial theme const initialTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); // Toggle theme await page.click('#theme-toggle'); // Verify theme changed const newTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme') ); expect(newTheme).not.toBe(initialTheme); }); });