678 lines
24 KiB
JavaScript
678 lines
24 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|
|
});
|