869 lines
30 KiB
JavaScript
869 lines
30 KiB
JavaScript
/**
|
||
* Unit tests for Queue UI functionality
|
||
* Tests queue management, button handlers, display updates, and statistics
|
||
*/
|
||
|
||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||
|
||
// Mock DOM setup
|
||
function setupDOM() {
|
||
document.body.innerHTML = `
|
||
<!-- Queue controls -->
|
||
<button id="start-queue-btn">Start Queue</button>
|
||
<button id="stop-queue-btn">Stop Queue</button>
|
||
<button id="clear-completed-btn">Clear Completed</button>
|
||
<button id="clear-failed-btn">Clear Failed</button>
|
||
<button id="clear-pending-btn">Clear Pending</button>
|
||
<button id="retry-all-btn">Retry All</button>
|
||
|
||
<!-- Queue statistics -->
|
||
<span id="pending-count">0</span>
|
||
<span id="active-count">0</span>
|
||
<span id="completed-count">0</span>
|
||
<span id="failed-count">0</span>
|
||
<span id="total-count">0</span>
|
||
|
||
<!-- Queue display -->
|
||
<div id="pending-queue"></div>
|
||
<div id="active-downloads"></div>
|
||
<div id="completed-queue"></div>
|
||
<div id="failed-queue"></div>
|
||
|
||
<!-- Modal -->
|
||
<div id="confirm-modal" class="modal" style="display: none;">
|
||
<div class="modal-overlay"></div>
|
||
<div class="modal-content">
|
||
<h3 id="confirm-title"></h3>
|
||
<p id="confirm-message"></p>
|
||
<button id="confirm-ok">OK</button>
|
||
<button id="confirm-cancel">Cancel</button>
|
||
<button id="close-confirm">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast -->
|
||
<div id="toast" class="toast"></div>
|
||
`;
|
||
}
|
||
|
||
// 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 =>
|
||
`<div class="queue-item" data-id="${item.id}">${item.serie_name} - Episode ${item.episode}</div>`
|
||
).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 =>
|
||
`<div class="queue-item" data-id="${item.id}">
|
||
${item.serie_name} - Episode ${item.episode}
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" style="width: ${item.progress}%"></div>
|
||
</div>
|
||
</div>`
|
||
).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 =>
|
||
`<div class="queue-item" data-id="${item.id}">${item.serie_name} - Episode ${item.episode}</div>`
|
||
).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 =>
|
||
`<div class="queue-item" data-id="${item.id}">
|
||
${item.serie_name} - Episode ${item.episode}
|
||
<span class="error">${item.error}</span>
|
||
</div>`
|
||
).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 = '<div>Item 1</div><div>Item 2</div>';
|
||
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 = `
|
||
<div class="queue-item" data-id="download-123">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" style="width: 0%"></div>
|
||
</div>
|
||
<span class="progress-text">0%</span>
|
||
</div>
|
||
`;
|
||
|
||
// 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 = `
|
||
<div class="queue-item" data-id="download-123">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" style="width: 0%"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="queue-item" data-id="item-1"></div>
|
||
<div class="queue-item" data-id="item-2"></div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="queue-item" data-id="download-123">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" style="width: 0%"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="queue-item" data-id="download-123">
|
||
<!-- No progress bar -->
|
||
</div>
|
||
`;
|
||
|
||
const card = activeQueue.querySelector('[data-id="download-123"]');
|
||
const progressFill = card.querySelector('.progress-fill');
|
||
|
||
expect(progressFill).toBeNull();
|
||
});
|
||
});
|