fix: resolve download client filter element ID mismatch and bind Select All/Deselect All (closes #38)
Build and Push Docker Image / build (push) Successful in 40s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m34s
CI / Security audit (push) Successful in 1m52s
CI / Swagger Validation & Coverage (push) Successful in 2m19s
CI / Tests & coverage (push) Successful in 2m31s
Build and Push Docker Image / build (push) Successful in 40s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m34s
CI / Security audit (push) Successful in 1m52s
CI / Swagger Validation & Coverage (push) Successful in 2m19s
CI / Tests & coverage (push) Successful in 2m31s
This commit is contained in:
+35
-11
@@ -5,9 +5,10 @@ import { saveDownloadClients } from '../utils/storage.js';
|
||||
import { renderDownloads } from './downloads.js';
|
||||
|
||||
export function initDownloadClientFilter() {
|
||||
const filterBtn = document.getElementById('download-client-filter-btn');
|
||||
const filterDropdown = document.getElementById('download-client-filter-dropdown');
|
||||
const filterClose = document.getElementById('download-client-filter-close');
|
||||
const filterBtn = document.getElementById('download-client-dropdown-btn');
|
||||
const filterDropdown = document.getElementById('download-client-dropdown');
|
||||
const selectAllBtn = document.getElementById('download-client-select-all');
|
||||
const deselectAllBtn = document.getElementById('download-client-deselect-all');
|
||||
|
||||
if (!filterBtn || !filterDropdown) return;
|
||||
|
||||
@@ -16,13 +17,16 @@ export function initDownloadClientFilter() {
|
||||
filterDropdown.classList.toggle('open');
|
||||
});
|
||||
|
||||
filterClose.addEventListener('click', () => {
|
||||
filterDropdown.classList.remove('open');
|
||||
});
|
||||
if (selectAllBtn) {
|
||||
selectAllBtn.addEventListener('click', () => toggleAllClients(true));
|
||||
}
|
||||
if (deselectAllBtn) {
|
||||
deselectAllBtn.addEventListener('click', () => toggleAllClients(false));
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!filterDropdown.contains(e.target) && e.target !== filterBtn) {
|
||||
if (!filterDropdown.contains(e.target) && e.target !== filterBtn && !filterBtn.contains(e.target)) {
|
||||
filterDropdown.classList.remove('open');
|
||||
}
|
||||
});
|
||||
@@ -35,7 +39,7 @@ export function initDownloadClientFilter() {
|
||||
}
|
||||
|
||||
export function updateDownloadClientFilter() {
|
||||
const filterList = document.getElementById('download-client-filter-list');
|
||||
const filterList = document.getElementById('download-client-options');
|
||||
if (!filterList) return;
|
||||
|
||||
filterList.innerHTML = '';
|
||||
@@ -75,13 +79,33 @@ export function toggleClientSelection(index) {
|
||||
renderDownloads();
|
||||
}
|
||||
|
||||
export function toggleAllClients(select) {
|
||||
if (select) {
|
||||
state.selectedDownloadClients = state.downloadClients.map((_, index) => index);
|
||||
} else {
|
||||
state.selectedDownloadClients = [];
|
||||
}
|
||||
saveDownloadClients(state.selectedDownloadClients);
|
||||
updateDownloadClientFilter();
|
||||
renderDownloads();
|
||||
}
|
||||
|
||||
export function updateSelectedCountDisplay() {
|
||||
const countDisplay = document.getElementById('download-client-filter-count');
|
||||
const countDisplay = document.getElementById('download-client-selected-text');
|
||||
if (!countDisplay) return;
|
||||
|
||||
if (state.selectedDownloadClients.length === 0) {
|
||||
countDisplay.textContent = 'All';
|
||||
countDisplay.textContent = 'All clients';
|
||||
} else if (state.selectedDownloadClients.length === state.downloadClients.length) {
|
||||
countDisplay.textContent = 'All clients';
|
||||
} else {
|
||||
countDisplay.textContent = state.selectedDownloadClients.length;
|
||||
const names = state.selectedDownloadClients
|
||||
.map(idx => state.downloadClients[idx]?.name || state.downloadClients[idx]?.type || '')
|
||||
.filter(Boolean);
|
||||
if (names.length === 1) {
|
||||
countDisplay.textContent = names[0];
|
||||
} else {
|
||||
countDisplay.textContent = `${state.selectedDownloadClients.length} clients`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
File diff suppressed because one or more lines are too long
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
* Tests for client/src/ui/filters.js
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { state } from '../../../client/src/state.js';
|
||||
import { initDownloadClientFilter, updateDownloadClientFilter, toggleClientSelection, toggleAllClients, updateSelectedCountDisplay } from '../../../client/src/ui/filters.js';
|
||||
import { renderDownloads } from '../../../client/src/ui/downloads.js';
|
||||
|
||||
// Mock renderDownloads to verify re-render triggers
|
||||
vi.mock('../../../client/src/ui/downloads.js', () => ({
|
||||
renderDownloads: vi.fn()
|
||||
}));
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store = {};
|
||||
return {
|
||||
getItem: (key) => store[key] || null,
|
||||
setItem: (key, value) => { store[key] = value; },
|
||||
removeItem: (key) => { delete store[key]; },
|
||||
clear: () => { store = {}; }
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock
|
||||
});
|
||||
|
||||
function setupDOM() {
|
||||
document.body.innerHTML = `
|
||||
<div class="downloads-controls">
|
||||
<div class="download-client-filter" id="download-client-filter">
|
||||
<button class="download-client-dropdown-btn" id="download-client-dropdown-btn" type="button">
|
||||
<span id="download-client-selected-text">All clients</span>
|
||||
<span class="dropdown-arrow">▼</span>
|
||||
</button>
|
||||
<div class="download-client-dropdown" id="download-client-dropdown">
|
||||
<div class="download-client-dropdown-header">
|
||||
<button id="download-client-select-all" type="button">Select All</button>
|
||||
<button id="download-client-deselect-all" type="button">Deselect All</button>
|
||||
</div>
|
||||
<div class="download-client-options" id="download-client-options">
|
||||
<!-- Options will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
describe('initDownloadClientFilter', () => {
|
||||
beforeEach(() => {
|
||||
localStorageMock.clear();
|
||||
state.downloadClients = [
|
||||
{ id: 1, type: 'sabnzbd', name: 'SABnzbd' },
|
||||
{ id: 2, type: 'qbittorrent', name: 'qBittorrent' }
|
||||
];
|
||||
state.selectedDownloadClients = [];
|
||||
vi.clearAllMocks();
|
||||
setupDOM();
|
||||
initDownloadClientFilter();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('populates options list with checkboxes matching download clients', () => {
|
||||
const optionsList = document.getElementById('download-client-options');
|
||||
expect(optionsList.children.length).toBe(2);
|
||||
|
||||
const firstItem = optionsList.children[0];
|
||||
const checkbox = firstItem.querySelector('input');
|
||||
const label = firstItem.querySelector('label');
|
||||
|
||||
expect(checkbox.type).toBe('checkbox');
|
||||
expect(checkbox.checked).toBe(false);
|
||||
expect(label.textContent).toBe('SABnzbd');
|
||||
});
|
||||
|
||||
it('restores checked state based on state.selectedDownloadClients', () => {
|
||||
state.selectedDownloadClients = [0];
|
||||
updateDownloadClientFilter();
|
||||
|
||||
const optionsList = document.getElementById('download-client-options');
|
||||
const firstCheckbox = optionsList.children[0].querySelector('input');
|
||||
const secondCheckbox = optionsList.children[1].querySelector('input');
|
||||
|
||||
expect(firstCheckbox.checked).toBe(true);
|
||||
expect(secondCheckbox.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('clicking a checkbox updates selected state and triggers re-render', () => {
|
||||
const optionsList = document.getElementById('download-client-options');
|
||||
const firstCheckbox = optionsList.children[0].querySelector('input');
|
||||
|
||||
firstCheckbox.click();
|
||||
|
||||
expect(state.selectedDownloadClients).toEqual([0]);
|
||||
expect(localStorageMock.getItem('sofarr-download-clients')).toBe(JSON.stringify([0]));
|
||||
expect(renderDownloads).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('select all selects all clients and saves to storage', () => {
|
||||
document.getElementById('download-client-select-all').click();
|
||||
|
||||
expect(state.selectedDownloadClients).toEqual([0, 1]);
|
||||
expect(localStorageMock.getItem('sofarr-download-clients')).toBe(JSON.stringify([0, 1]));
|
||||
expect(renderDownloads).toHaveBeenCalled();
|
||||
|
||||
const optionsList = document.getElementById('download-client-options');
|
||||
expect(optionsList.children[0].querySelector('input').checked).toBe(true);
|
||||
expect(optionsList.children[1].querySelector('input').checked).toBe(true);
|
||||
});
|
||||
|
||||
it('deselect all clears all clients and saves empty list to storage', () => {
|
||||
state.selectedDownloadClients = [0, 1];
|
||||
updateDownloadClientFilter();
|
||||
|
||||
document.getElementById('download-client-deselect-all').click();
|
||||
|
||||
expect(state.selectedDownloadClients).toEqual([]);
|
||||
expect(localStorageMock.getItem('sofarr-download-clients')).toBe(JSON.stringify([]));
|
||||
expect(renderDownloads).toHaveBeenCalled();
|
||||
|
||||
const optionsList = document.getElementById('download-client-options');
|
||||
expect(optionsList.children[0].querySelector('input').checked).toBe(false);
|
||||
expect(optionsList.children[1].querySelector('input').checked).toBe(false);
|
||||
});
|
||||
|
||||
it('toggles dropdown when dropdown button is clicked', () => {
|
||||
const dropdown = document.getElementById('download-client-dropdown');
|
||||
const btn = document.getElementById('download-client-dropdown-btn');
|
||||
|
||||
btn.click();
|
||||
expect(dropdown.classList.contains('open')).toBe(true);
|
||||
|
||||
btn.click();
|
||||
expect(dropdown.classList.contains('open')).toBe(false);
|
||||
});
|
||||
|
||||
it('closes dropdown when clicking outside', () => {
|
||||
const dropdown = document.getElementById('download-client-dropdown');
|
||||
const btn = document.getElementById('download-client-dropdown-btn');
|
||||
|
||||
btn.click();
|
||||
expect(dropdown.classList.contains('open')).toBe(true);
|
||||
|
||||
document.body.click();
|
||||
expect(dropdown.classList.contains('open')).toBe(false);
|
||||
});
|
||||
|
||||
it('updates selected text display correctly based on count', () => {
|
||||
const selectedText = document.getElementById('download-client-selected-text');
|
||||
|
||||
state.selectedDownloadClients = [];
|
||||
updateSelectedCountDisplay();
|
||||
expect(selectedText.textContent).toBe('All clients');
|
||||
|
||||
state.selectedDownloadClients = [0];
|
||||
updateSelectedCountDisplay();
|
||||
expect(selectedText.textContent).toBe('SABnzbd');
|
||||
|
||||
state.selectedDownloadClients = [0, 1];
|
||||
updateSelectedCountDisplay();
|
||||
expect(selectedText.textContent).toBe('All clients'); // Since it's all of them
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user