/** * Unit tests for Queue UI functionality * Tests queue management, button handlers, display updates, and statistics */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // Mock DOM setup function setupDOM() { document.body.innerHTML = ` 0 0 0 0 0
`; } // Mock AniWorld global object function setupMockAniWorld() { global.AniWorld = { Constants: { API: { QUEUE_STATUS: '/api/queue/status', QUEUE_START: '/api/queue/start', QUEUE_STOP: '/api/queue/stop', QUEUE_REMOVE: '/api/queue/remove', QUEUE_RETRY: '/api/queue/retry', QUEUE_COMPLETED: '/api/queue/completed', QUEUE_FAILED: '/api/queue/failed', QUEUE_PENDING: '/api/queue/pending' } }, ApiClient: { get: vi.fn(), post: vi.fn(), delete: vi.fn() }, UI: { showConfirmModal: vi.fn(), hideConfirmModal: vi.fn(), showToast: vi.fn() }, Theme: { init: vi.fn(), toggle: vi.fn() }, Auth: { checkAuth: vi.fn().mockResolvedValue(true), logout: vi.fn() }, WebSocketClient: { init: vi.fn() }, QueueSocketHandler: { init: vi.fn() }, QueueRenderer: { updateQueueDisplay: vi.fn(), updateDownloadProgress: vi.fn(), renderQueueItem: vi.fn() }, ProgressHandler: { processPendingProgressUpdates: vi.fn(), updateProgress: vi.fn() }, QueueAPI: { loadQueueData: vi.fn(), startQueue: vi.fn(), stopQueue: vi.fn(), removeFromQueue: vi.fn(), retryDownloads: vi.fn(), clearCompleted: vi.fn(), clearFailed: vi.fn(), clearPending: vi.fn() } }; } describe('Queue API - Data Loading', () => { beforeEach(() => { setupDOM(); setupMockAniWorld(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should load queue data successfully', async () => { const mockData = { status: { is_running: false, current_download: null, pending_items: [], active_downloads: [], completed_items: [], failed_items: [] }, statistics: { pending: 0, active: 0, completed: 0, failed: 0, total: 0 } }; const mockResponse = { json: vi.fn().mockResolvedValue(mockData) }; global.AniWorld.ApiClient.get.mockResolvedValue(mockResponse); const data = await global.AniWorld.QueueAPI.loadQueueData(); expect(global.AniWorld.ApiClient.get).toHaveBeenCalledWith('/api/queue/status'); expect(data).toHaveProperty('statistics'); expect(data.statistics.total).toBe(0); }); it('should handle API error gracefully', async () => { global.AniWorld.ApiClient.get.mockRejectedValue(new Error('Network error')); const data = await global.AniWorld.QueueAPI.loadQueueData(); expect(data).toBeNull(); }); it('should transform nested API response structure', async () => { const mockData = { status: { is_running: true, pending_items: [{ id: '1' }] }, statistics: { pending: 1, active: 0, completed: 0, failed: 0, total: 1 } }; const mockResponse = { json: vi.fn().mockResolvedValue(mockData) }; global.AniWorld.ApiClient.get.mockResolvedValue(mockResponse); const data = await global.AniWorld.QueueAPI.loadQueueData(); expect(data.is_running).toBe(true); expect(data.pending_items).toHaveLength(1); expect(data.statistics.pending).toBe(1); }); }); describe('Queue API - Queue Control', () => { beforeEach(() => { setupDOM(); setupMockAniWorld(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should start queue successfully', async () => { const mockResponse = { json: vi.fn().mockResolvedValue({ message: 'Queue started' }) }; global.AniWorld.ApiClient.post.mockResolvedValue(mockResponse); const result = await global.AniWorld.QueueAPI.startQueue(); expect(global.AniWorld.ApiClient.post).toHaveBeenCalledWith('/api/queue/start', {}); expect(result.message).toBe('Queue started'); }); it('should stop queue successfully', async () => { const mockResponse = { json: vi.fn().mockResolvedValue({ message: 'Queue stopped' }) }; global.AniWorld.ApiClient.post.mockResolvedValue(mockResponse); const result = await global.AniWorld.QueueAPI.stopQueue(); expect(global.AniWorld.ApiClient.post).toHaveBeenCalledWith('/api/queue/stop', {}); expect(result.message).toBe('Queue stopped'); }); it('should handle start queue error', async () => { global.AniWorld.ApiClient.post.mockRejectedValue(new Error('Already running')); await expect(global.AniWorld.QueueAPI.startQueue()).rejects.toThrow('Already running'); }); it('should handle stop queue error', async () => { global.AniWorld.ApiClient.post.mockRejectedValue(new Error('Not running')); await expect(global.AniWorld.QueueAPI.stopQueue()).rejects.toThrow('Not running'); }); }); describe('Queue API - Item Management', () => { beforeEach(() => { setupDOM(); setupMockAniWorld(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should remove item from queue', async () => { const mockResponse = { status: 204 }; global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse); const result = await global.AniWorld.QueueAPI.removeFromQueue('item-123'); expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/remove/item-123'); expect(result).toBe(true); }); it('should retry failed downloads', async () => { const mockResponse = { json: vi.fn().mockResolvedValue({ retried: 2 }) }; global.AniWorld.ApiClient.post.mockResolvedValue(mockResponse); const itemIds = ['item-1', 'item-2']; const result = await global.AniWorld.QueueAPI.retryDownloads(itemIds); expect(global.AniWorld.ApiClient.post).toHaveBeenCalledWith('/api/queue/retry', { item_ids: itemIds }); expect(result.retried).toBe(2); }); it('should clear completed downloads', async () => { const mockResponse = { json: vi.fn().mockResolvedValue({ cleared: 5 }) }; global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse); const result = await global.AniWorld.QueueAPI.clearCompleted(); expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/completed'); expect(result.cleared).toBe(5); }); it('should clear failed downloads', async () => { const mockResponse = { json: vi.fn().mockResolvedValue({ cleared: 3 }) }; global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse); const result = await global.AniWorld.QueueAPI.clearFailed(); expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/failed'); expect(result.cleared).toBe(3); }); it('should clear pending downloads', async () => { const mockResponse = { json: vi.fn().mockResolvedValue({ cleared: 2 }) }; global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse); const result = await global.AniWorld.QueueAPI.clearPending(); expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/pending'); expect(result.cleared).toBe(2); }); }); describe('Queue Renderer - Statistics Display', () => { beforeEach(() => { setupDOM(); setupMockAniWorld(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should update queue statistics display', () => { const data = { statistics: { pending: 5, active: 2, completed: 10, failed: 1, total: 18 } }; // Manually update DOM (simulating renderer behavior) document.getElementById('pending-count').textContent = data.statistics.pending; document.getElementById('active-count').textContent = data.statistics.active; document.getElementById('completed-count').textContent = data.statistics.completed; document.getElementById('failed-count').textContent = data.statistics.failed; document.getElementById('total-count').textContent = data.statistics.total; expect(document.getElementById('pending-count').textContent).toBe('5'); expect(document.getElementById('active-count').textContent).toBe('2'); expect(document.getElementById('completed-count').textContent).toBe('10'); expect(document.getElementById('failed-count').textContent).toBe('1'); expect(document.getElementById('total-count').textContent).toBe('18'); }); it('should handle zero statistics', () => { const data = { statistics: { pending: 0, active: 0, completed: 0, failed: 0, total: 0 } }; document.getElementById('pending-count').textContent = data.statistics.pending; document.getElementById('active-count').textContent = data.statistics.active; document.getElementById('completed-count').textContent = data.statistics.completed; document.getElementById('failed-count').textContent = data.statistics.failed; document.getElementById('total-count').textContent = data.statistics.total; expect(document.getElementById('pending-count').textContent).toBe('0'); expect(document.getElementById('active-count').textContent).toBe('0'); expect(document.getElementById('completed-count').textContent).toBe('0'); expect(document.getElementById('failed-count').textContent).toBe('0'); expect(document.getElementById('total-count').textContent).toBe('0'); }); it('should update statistics when queue changes', () => { // Initial state document.getElementById('pending-count').textContent = '5'; expect(document.getElementById('pending-count').textContent).toBe('5'); // After starting download (one moves from pending to active) document.getElementById('pending-count').textContent = '4'; document.getElementById('active-count').textContent = '1'; expect(document.getElementById('pending-count').textContent).toBe('4'); expect(document.getElementById('active-count').textContent).toBe('1'); }); }); describe('Queue Renderer - Queue Display', () => { beforeEach(() => { setupDOM(); setupMockAniWorld(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should render pending queue items', () => { const pendingItems = [ { id: '1', serie_name: 'Anime 1', episode: 1, status: 'pending' }, { id: '2', serie_name: 'Anime 2', episode: 2, status: 'pending' } ]; const pendingQueue = document.getElementById('pending-queue'); pendingQueue.innerHTML = pendingItems.map(item => `
${item.serie_name} - Episode ${item.episode}
` ).join(''); expect(pendingQueue.children.length).toBe(2); expect(pendingQueue.children[0].textContent).toBe('Anime 1 - Episode 1'); expect(pendingQueue.children[1].textContent).toBe('Anime 2 - Episode 2'); }); it('should render active downloads with progress', () => { const activeDownloads = [ { id: '1', serie_name: 'Anime 1', episode: 1, progress: 45, status: 'active' } ]; const activeQueue = document.getElementById('active-downloads'); activeQueue.innerHTML = activeDownloads.map(item => `
${item.serie_name} - Episode ${item.episode}
` ).join(''); expect(activeQueue.children.length).toBe(1); const progressFill = activeQueue.querySelector('.progress-fill'); expect(progressFill.style.width).toBe('45%'); }); it('should render completed queue items', () => { const completedItems = [ { id: '1', serie_name: 'Anime 1', episode: 1, status: 'completed' }, { id: '2', serie_name: 'Anime 2', episode: 2, status: 'completed' } ]; const completedQueue = document.getElementById('completed-queue'); completedQueue.innerHTML = completedItems.map(item => `
${item.serie_name} - Episode ${item.episode}
` ).join(''); expect(completedQueue.children.length).toBe(2); }); it('should render failed queue items', () => { const failedItems = [ { id: '1', serie_name: 'Anime 1', episode: 1, status: 'failed', error: 'Network error' } ]; const failedQueue = document.getElementById('failed-queue'); failedQueue.innerHTML = failedItems.map(item => `
${item.serie_name} - Episode ${item.episode} ${item.error}
` ).join(''); expect(failedQueue.children.length).toBe(1); expect(failedQueue.querySelector('.error').textContent).toBe('Network error'); }); it('should clear queue display when empty', () => { const pendingQueue = document.getElementById('pending-queue'); pendingQueue.innerHTML = '
Item 1
Item 2
'; expect(pendingQueue.children.length).toBe(2); // Clear display pendingQueue.innerHTML = ''; expect(pendingQueue.children.length).toBe(0); }); }); describe('Queue Progress Handler', () => { beforeEach(() => { setupDOM(); setupMockAniWorld(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should update progress bar for active download', () => { // Setup active download card const activeQueue = document.getElementById('active-downloads'); activeQueue.innerHTML = `
0%
`; // Simulate progress update const progressData = { item_id: 'download-123', progress: 75 }; const card = activeQueue.querySelector(`[data-id="${progressData.item_id}"]`); const progressFill = card.querySelector('.progress-fill'); const progressText = card.querySelector('.progress-text'); progressFill.style.width = `${progressData.progress}%`; progressText.textContent = `${progressData.progress}%`; expect(progressFill.style.width).toBe('75%'); expect(progressText.textContent).toBe('75%'); }); it('should handle progress update for non-existent card', () => { const activeQueue = document.getElementById('active-downloads'); const progressData = { item_id: 'nonexistent-123', progress: 50 }; const card = activeQueue.querySelector(`[data-id="${progressData.item_id}"]`); expect(card).toBeNull(); }); it('should update progress from 0 to 100', () => { const activeQueue = document.getElementById('active-downloads'); activeQueue.innerHTML = `
`; const card = activeQueue.querySelector('[data-id="download-123"]'); const progressFill = card.querySelector('.progress-fill'); // Simulate progress updates [0, 25, 50, 75, 100].forEach(progress => { progressFill.style.width = `${progress}%`; expect(progressFill.style.width).toBe(`${progress}%`); }); }); }); describe('Queue Button Handlers', () => { beforeEach(() => { setupDOM(); setupMockAniWorld(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should call start queue on button click', async () => { const mockResponse = { json: vi.fn().mockResolvedValue({ message: 'Queue started' }) }; global.AniWorld.ApiClient.post.mockResolvedValue(mockResponse); const button = document.getElementById('start-queue-btn'); const handler = async () => { await global.AniWorld.QueueAPI.startQueue(); global.AniWorld.UI.showToast('Queue started', 'success'); }; button.addEventListener('click', handler); button.click(); await new Promise(resolve => setTimeout(resolve, 0)); expect(global.AniWorld.ApiClient.post).toHaveBeenCalledWith('/api/queue/start', {}); }); it('should call stop queue on button click', async () => { const mockResponse = { json: vi.fn().mockResolvedValue({ message: 'Queue stopped' }) }; global.AniWorld.ApiClient.post.mockResolvedValue(mockResponse); const button = document.getElementById('stop-queue-btn'); const handler = async () => { await global.AniWorld.QueueAPI.stopQueue(); global.AniWorld.UI.showToast('Queue stopped', 'success'); }; button.addEventListener('click', handler); button.click(); await new Promise(resolve => setTimeout(resolve, 0)); expect(global.AniWorld.ApiClient.post).toHaveBeenCalledWith('/api/queue/stop', {}); }); it('should show confirmation before clearing completed', async () => { global.AniWorld.UI.showConfirmModal.mockResolvedValue(true); const mockResponse = { json: vi.fn().mockResolvedValue({ cleared: 5 }) }; global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse); const button = document.getElementById('clear-completed-btn'); const handler = async () => { const confirmed = await global.AniWorld.UI.showConfirmModal( 'Clear Completed Downloads', 'Are you sure you want to clear all completed downloads?' ); if (confirmed) { await global.AniWorld.QueueAPI.clearCompleted(); } }; button.addEventListener('click', handler); button.click(); await new Promise(resolve => setTimeout(resolve, 0)); expect(global.AniWorld.UI.showConfirmModal).toHaveBeenCalled(); expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/completed'); }); it('should not clear completed if confirmation cancelled', async () => { global.AniWorld.UI.showConfirmModal.mockResolvedValue(false); const button = document.getElementById('clear-completed-btn'); const handler = async () => { const confirmed = await global.AniWorld.UI.showConfirmModal( 'Clear Completed Downloads', 'Are you sure?' ); if (confirmed) { await global.AniWorld.QueueAPI.clearCompleted(); } }; button.addEventListener('click', handler); button.click(); await new Promise(resolve => setTimeout(resolve, 0)); expect(global.AniWorld.UI.showConfirmModal).toHaveBeenCalled(); expect(global.AniWorld.ApiClient.delete).not.toHaveBeenCalled(); }); it('should show confirmation before clearing failed', async () => { global.AniWorld.UI.showConfirmModal.mockResolvedValue(true); const mockResponse = { json: vi.fn().mockResolvedValue({ cleared: 3 }) }; global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse); const button = document.getElementById('clear-failed-btn'); const handler = async () => { const confirmed = await global.AniWorld.UI.showConfirmModal( 'Clear Failed Downloads', 'Are you sure you want to clear all failed downloads?' ); if (confirmed) { await global.AniWorld.QueueAPI.clearFailed(); } }; button.addEventListener('click', handler); button.click(); await new Promise(resolve => setTimeout(resolve, 0)); expect(global.AniWorld.UI.showConfirmModal).toHaveBeenCalled(); expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/failed'); }); it('should show confirmation before clearing pending', async () => { global.AniWorld.UI.showConfirmModal.mockResolvedValue(true); const mockResponse = { json: vi.fn().mockResolvedValue({ cleared: 2 }) }; global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse); const button = document.getElementById('clear-pending-btn'); const handler = async () => { const confirmed = await global.AniWorld.UI.showConfirmModal( 'Remove All Pending Downloads', 'Are you sure you want to remove all pending downloads from the queue?' ); if (confirmed) { await global.AniWorld.QueueAPI.clearPending(); } }; button.addEventListener('click', handler); button.click(); await new Promise(resolve => setTimeout(resolve, 0)); expect(global.AniWorld.UI.showConfirmModal).toHaveBeenCalled(); expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/pending'); }); it('should retry all failed downloads', async () => { global.AniWorld.UI.showConfirmModal.mockResolvedValue(true); const mockResponse = { json: vi.fn().mockResolvedValue({ retried: 2 }) }; global.AniWorld.ApiClient.post.mockResolvedValue(mockResponse); // Simulate failed items in DOM const failedQueue = document.getElementById('failed-queue'); failedQueue.innerHTML = `
`; const button = document.getElementById('retry-all-btn'); const handler = async () => { const failedItems = Array.from(failedQueue.querySelectorAll('.queue-item')); const itemIds = failedItems.map(item => item.dataset.id); const confirmed = await global.AniWorld.UI.showConfirmModal( 'Retry Failed Downloads', `Retry ${itemIds.length} failed downloads?` ); if (confirmed && itemIds.length > 0) { await global.AniWorld.QueueAPI.retryDownloads(itemIds); } }; button.addEventListener('click', handler); button.click(); await new Promise(resolve => setTimeout(resolve, 0)); expect(global.AniWorld.UI.showConfirmModal).toHaveBeenCalled(); expect(global.AniWorld.ApiClient.post).toHaveBeenCalledWith( '/api/queue/retry', { item_ids: ['item-1', 'item-2'] } ); }); }); describe('Queue Real-time Updates', () => { beforeEach(() => { setupDOM(); setupMockAniWorld(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should update display on queue_updated event', () => { const updateHandler = vi.fn(); // Simulate WebSocket event const data = { statistics: { pending: 3, active: 1, completed: 5, failed: 0, total: 9 } }; updateHandler(data); expect(updateHandler).toHaveBeenCalledWith(data); }); it('should update display on download_progress event', () => { const progressHandler = vi.fn(); const progressData = { item_id: 'download-123', progress: 65, speed: '5.2 MB/s', eta: '2 minutes' }; progressHandler(progressData); expect(progressHandler).toHaveBeenCalledWith(progressData); }); it('should reload queue on download_completed event', () => { const reloadHandler = vi.fn(); const completedData = { item_id: 'download-123', serie_name: 'Anime 1', episode: 1 }; reloadHandler(completedData); expect(reloadHandler).toHaveBeenCalledWith(completedData); }); it('should reload queue on download_failed event', () => { const reloadHandler = vi.fn(); const failedData = { item_id: 'download-123', error: 'Network timeout' }; reloadHandler(failedData); expect(reloadHandler).toHaveBeenCalledWith(failedData); }); }); describe('Queue Edge Cases', () => { beforeEach(() => { setupDOM(); setupMockAniWorld(); }); afterEach(() => { vi.restoreAllMocks(); }); it('should handle empty queue gracefully', () => { const data = { statistics: { pending: 0, active: 0, completed: 0, failed: 0, total: 0 }, pending_items: [], active_downloads: [], completed_items: [], failed_items: [] }; document.getElementById('pending-count').textContent = data.statistics.pending; document.getElementById('pending-queue').innerHTML = ''; expect(document.getElementById('pending-count').textContent).toBe('0'); expect(document.getElementById('pending-queue').children.length).toBe(0); }); it('should handle rapid progress updates', () => { const activeQueue = document.getElementById('active-downloads'); activeQueue.innerHTML = `
`; const card = activeQueue.querySelector('[data-id="download-123"]'); const progressFill = card.querySelector('.progress-fill'); // Simulate rapid updates for (let i = 0; i <= 100; i += 5) { progressFill.style.width = `${i}%`; } expect(progressFill.style.width).toBe('100%'); }); it('should handle missing progress bar element', () => { const activeQueue = document.getElementById('active-downloads'); activeQueue.innerHTML = `
`; const card = activeQueue.querySelector('[data-id="download-123"]'); const progressFill = card.querySelector('.progress-fill'); expect(progressFill).toBeNull(); }); });