Files
Aniworld/tests/frontend/unit/queue_ui.test.js

869 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
});
});