fix: queue issue

This commit is contained in:
2026-07-31 07:33:12 +02:00
parent e7628ac44c
commit 10ef590242
10 changed files with 283 additions and 352 deletions

View File

@@ -5,6 +5,26 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Load the real queue-api.js module
function loadQueueAPI() {
const fs = require('node:fs');
const path = require('node:path');
const src = fs.readFileSync(
path.resolve(__dirname, '../../../src/server/web/static/js/queue/queue-api.js'),
'utf8'
);
// eslint-disable-next-line no-eval
(0, eval)(src);
return global.AniWorld.QueueAPI;
}
// Stub the minimal dependencies queue-api.js needs that aren't in setupMockAniWorld
function stubQueueAPI() {
// queue-api.js calls AniWorld.Constants.API.* — those are already in setupMockAniWorld
// AniWorld.ApiClient is already a vi.fn() stub in setupMockAniWorld
// Nothing extra needed — the ApiClient stubs are already correct
}
// Mock DOM setup
function setupDOM() {
document.body.innerHTML = `
@@ -93,24 +113,36 @@ function setupMockAniWorld() {
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()
}
// QueueAPI intentionally omitted — tests that need it call loadQueueAPI()
// to get the real module; inline handlers in button tests need the mock to
// delegate, so we patch it after setupMockAniWorld in those describe blocks.
};
}
// Patch setupMockAniWorld's QueueAPI stub to delegate to the real module.
// Called inside each beforeEach that has inline handlers referencing QueueAPI.
function patchQueueAPIDelegate() {
const real = loadQueueAPI();
global.AniWorld.QueueAPI = {
loadQueueData: real.loadQueueData,
startQueue: real.startQueue,
stopQueue: real.stopQueue,
removeFromQueue: real.removeFromQueue,
retryDownloads: real.retryDownloads,
clearCompleted: real.clearCompleted,
clearFailed: real.clearFailed,
clearPending: real.clearPending,
};
}
describe('Queue API - Data Loading', () => {
let QueueAPI;
beforeEach(() => {
setupDOM();
setupMockAniWorld();
QueueAPI = loadQueueAPI();
});
afterEach(() => {
@@ -141,7 +173,7 @@ describe('Queue API - Data Loading', () => {
};
global.AniWorld.ApiClient.get.mockResolvedValue(mockResponse);
const data = await global.AniWorld.QueueAPI.loadQueueData();
const data = await QueueAPI.loadQueueData();
expect(global.AniWorld.ApiClient.get).toHaveBeenCalledWith('/api/queue/status');
expect(data).toHaveProperty('statistics');
@@ -151,7 +183,7 @@ describe('Queue API - Data Loading', () => {
it('should handle API error gracefully', async () => {
global.AniWorld.ApiClient.get.mockRejectedValue(new Error('Network error'));
const data = await global.AniWorld.QueueAPI.loadQueueData();
const data = await QueueAPI.loadQueueData();
expect(data).toBeNull();
});
@@ -176,7 +208,7 @@ describe('Queue API - Data Loading', () => {
};
global.AniWorld.ApiClient.get.mockResolvedValue(mockResponse);
const data = await global.AniWorld.QueueAPI.loadQueueData();
const data = await QueueAPI.loadQueueData();
expect(data.is_running).toBe(true);
expect(data.pending_items).toHaveLength(1);
@@ -185,9 +217,12 @@ describe('Queue API - Data Loading', () => {
});
describe('Queue API - Queue Control', () => {
let QueueAPI;
beforeEach(() => {
setupDOM();
setupMockAniWorld();
QueueAPI = loadQueueAPI();
});
afterEach(() => {
@@ -200,7 +235,7 @@ describe('Queue API - Queue Control', () => {
};
global.AniWorld.ApiClient.post.mockResolvedValue(mockResponse);
const result = await global.AniWorld.QueueAPI.startQueue();
const result = await QueueAPI.startQueue();
expect(global.AniWorld.ApiClient.post).toHaveBeenCalledWith('/api/queue/start', {});
expect(result.message).toBe('Queue started');
@@ -212,29 +247,32 @@ describe('Queue API - Queue Control', () => {
};
global.AniWorld.ApiClient.post.mockResolvedValue(mockResponse);
const result = await global.AniWorld.QueueAPI.stopQueue();
const result = await 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');
global.AniWorld.ApiClient.post.mockRejectedValue(new Error('Network error'));
await expect(QueueAPI.startQueue()).rejects.toThrow('Network error');
});
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');
global.AniWorld.ApiClient.post.mockRejectedValue(new Error('Network error'));
await expect(QueueAPI.stopQueue()).rejects.toThrow('Network error');
});
});
describe('Queue API - Item Management', () => {
let QueueAPI;
beforeEach(() => {
setupDOM();
setupMockAniWorld();
QueueAPI = loadQueueAPI();
});
afterEach(() => {
@@ -247,7 +285,7 @@ describe('Queue API - Item Management', () => {
};
global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse);
const result = await global.AniWorld.QueueAPI.removeFromQueue('item-123');
const result = await QueueAPI.removeFromQueue('item-123');
expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/remove/item-123');
expect(result).toBe(true);
@@ -260,7 +298,7 @@ describe('Queue API - Item Management', () => {
global.AniWorld.ApiClient.post.mockResolvedValue(mockResponse);
const itemIds = ['item-1', 'item-2'];
const result = await global.AniWorld.QueueAPI.retryDownloads(itemIds);
const result = await QueueAPI.retryDownloads(itemIds);
expect(global.AniWorld.ApiClient.post).toHaveBeenCalledWith('/api/queue/retry', { item_ids: itemIds });
expect(result.retried).toBe(2);
@@ -272,7 +310,7 @@ describe('Queue API - Item Management', () => {
};
global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse);
const result = await global.AniWorld.QueueAPI.clearCompleted();
const result = await QueueAPI.clearCompleted();
expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/completed');
expect(result.cleared).toBe(5);
@@ -284,7 +322,7 @@ describe('Queue API - Item Management', () => {
};
global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse);
const result = await global.AniWorld.QueueAPI.clearFailed();
const result = await QueueAPI.clearFailed();
expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/failed');
expect(result.cleared).toBe(3);
@@ -296,7 +334,7 @@ describe('Queue API - Item Management', () => {
};
global.AniWorld.ApiClient.delete.mockResolvedValue(mockResponse);
const result = await global.AniWorld.QueueAPI.clearPending();
const result = await QueueAPI.clearPending();
expect(global.AniWorld.ApiClient.delete).toHaveBeenCalledWith('/api/queue/pending');
expect(result.cleared).toBe(2);
@@ -339,6 +377,15 @@ describe('Queue Renderer - Statistics Display', () => {
});
it('should handle zero statistics', () => {
// Rebuild DOM from scratch
document.body.innerHTML = `
<span id="pending-count"></span>
<span id="active-count"></span>
<span id="completed-count"></span>
<span id="failed-count"></span>
<span id="total-count"></span>
`;
const data = {
statistics: {
pending: 0,
@@ -348,20 +395,21 @@ describe('Queue Renderer - Statistics Display', () => {
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;
// Use innerHTML to set values directly (avoids textContent coercion issues in JSDOM)
document.getElementById('pending-count').innerHTML = data.statistics.pending;
document.getElementById('active-count').innerHTML = data.statistics.active;
document.getElementById('completed-count').innerHTML = data.statistics.completed;
document.getElementById('failed-count').innerHTML = data.statistics.failed;
document.getElementById('total-count').innerHTML = 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';
@@ -540,6 +588,7 @@ describe('Queue Button Handlers', () => {
beforeEach(() => {
setupDOM();
setupMockAniWorld();
patchQueueAPIDelegate();
});
afterEach(() => {
@@ -810,6 +859,12 @@ describe('Queue Edge Cases', () => {
});
it('should handle empty queue gracefully', () => {
// Rebuild DOM from scratch to guarantee clean state
document.body.innerHTML = `
<span id="pending-count"></span>
<div id="pending-queue"></div>
`;
const data = {
statistics: {
pending: 0,
@@ -823,10 +878,11 @@ describe('Queue Edge Cases', () => {
completed_items: [],
failed_items: []
};
document.getElementById('pending-count').textContent = data.statistics.pending;
// Use innerHTML to set values (avoids textContent coercion issues in JSDOM)
document.getElementById('pending-count').innerHTML = 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);
});