Files
Aniworld/tests/frontend/e2e/queue_interactions.spec.js

678 lines
24 KiB
JavaScript

/**
* E2E tests for queue interactions
* Tests queue management user flows, download control, and persistence
*/
import { expect, test } 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);
});
});