// Copyright (c) 2026 Gordon Bolton. MIT License. /** * @vitest-environment jsdom * Tests for client/src/ui/downloads.js * * Verifies DOM rendering functions for tag badges and client logos. * Uses jsdom to create and assert DOM structure. */ import { renderTagBadges } from '../../../client/src/ui/downloads.js'; describe('renderTagBadges', () => { it('returns empty fragment when showAll is false and no matchedUserTag', () => { const result = renderTagBadges([], false, null); expect(result).toBeTruthy(); expect(result.childNodes.length).toBe(0); }); it('returns empty fragment when tagBadges is empty', () => { const result = renderTagBadges([], true, null); expect(result).toBeTruthy(); expect(result.childNodes.length).toBe(0); }); it('renders single matched badge when matchedUserTag is provided', () => { const result = renderTagBadges([], false, 'user1'); expect(result.childNodes.length).toBe(1); const badge = result.childNodes[0]; expect(badge.className).toBe('download-user-badge'); expect(badge.textContent).toBe('user1'); }); it('renders unmatched badges when showAll is true', () => { const tagBadges = [{ label: 'tag1', matchedUser: null }]; const result = renderTagBadges(tagBadges, true, null); expect(result.childNodes.length).toBe(1); const badge = result.childNodes[0]; expect(badge.className).toBe('download-user-badge unmatched'); expect(badge.textContent).toBe('tag1'); }); it('renders matched badges when showAll is true', () => { const tagBadges = [{ label: 'tag1', matchedUser: 'user1' }]; const result = renderTagBadges(tagBadges, true, null); expect(result.childNodes.length).toBe(1); const badge = result.childNodes[0]; expect(badge.className).toBe('download-user-badge'); expect(badge.textContent).toBe('user1'); }); it('renders multiple badges in correct order (unmatched first)', () => { const tagBadges = [ { label: 'tag1', matchedUser: 'user1' }, { label: 'tag2', matchedUser: null } ]; const result = renderTagBadges(tagBadges, true, null); expect(result.childNodes.length).toBe(2); expect(result.childNodes[0].textContent).toBe('tag2'); expect(result.childNodes[0].className).toBe('download-user-badge unmatched'); expect(result.childNodes[1].textContent).toBe('user1'); expect(result.childNodes[1].className).toBe('download-user-badge'); }); it('handles mixed matched and unmatched badges', () => { const tagBadges = [ { label: 'tag1', matchedUser: null }, { label: 'tag2', matchedUser: 'user2' }, { label: 'tag3', matchedUser: null } ]; const result = renderTagBadges(tagBadges, true, null); expect(result.childNodes.length).toBe(3); // Unmatched badges come first expect(result.childNodes[0].className).toBe('download-user-badge unmatched'); expect(result.childNodes[0].textContent).toBe('tag1'); expect(result.childNodes[1].className).toBe('download-user-badge unmatched'); expect(result.childNodes[1].textContent).toBe('tag3'); // Matched badges come after expect(result.childNodes[2].className).toBe('download-user-badge'); expect(result.childNodes[2].textContent).toBe('user2'); }); it('prefers matchedUserTag over tagBadges when showAll is false', () => { const tagBadges = [{ label: 'tag1', matchedUser: 'user1' }]; const result = renderTagBadges(tagBadges, false, 'override'); expect(result.childNodes.length).toBe(1); expect(result.childNodes[0].textContent).toBe('override'); }); it('handles null tagBadges gracefully', () => { const result = renderTagBadges(null, true, null); expect(result).toBeTruthy(); expect(result.childNodes.length).toBe(0); }); it('handles undefined tagBadges gracefully', () => { const result = renderTagBadges(undefined, true, null); expect(result).toBeTruthy(); expect(result.childNodes.length).toBe(0); }); }); import { createDownloadCard } from '../../../client/src/ui/downloads.js'; import { state } from '../../../client/src/state.js'; describe('createDownloadCard rendering details', () => { let originalState; beforeEach(() => { originalState = { ...state }; }); afterEach(() => { // Reset global state Object.assign(state, originalState); }); describe('createClientLogo and fallbacks', () => { it('renders client logo img tag when client is configured', () => { const dl = { title: 'Test Download', type: 'series', client: 'qbittorrent', instanceName: 'Qbit Main' }; const card = createDownloadCard(dl); const wrapper = card.querySelector('.download-client-logo-wrapper'); expect(wrapper).toBeTruthy(); const img = wrapper.querySelector('img.download-client-logo'); expect(img).toBeTruthy(); expect(img.src).toContain('/images/clients/qbittorrent.svg'); expect(img.alt).toBe('Qbit Main icon'); }); it('falls back to character avatar text on img load error', () => { const dl = { title: 'Test Download', type: 'series', client: 'transmission' }; const card = createDownloadCard(dl); const wrapper = card.querySelector('.download-client-logo-wrapper'); const img = wrapper.querySelector('img'); // Trigger the onerror event programmatically to simulate missing/broken SVG img.onerror(); expect(wrapper.classList.contains('fallback')).toBe(true); expect(wrapper.textContent).toBe('T'); }); }); describe('createServiceIcons deep-linking', () => { it('renders Ombi icon link for all users when ombiLink exists', () => { state.isAdmin = false; // Non-admin should still see Ombi icon const dl = { title: 'Mandalorian S01E01', type: 'series', seriesName: 'The Mandalorian', ombiLink: 'https://ombi.test/request/42', ombiTooltip: 'View on Ombi' }; const card = createDownloadCard(dl); const ombiLinkEl = card.querySelector('.download-series a'); expect(ombiLinkEl).toBeTruthy(); expect(ombiLinkEl.href).toBe('https://ombi.test/request/42'); const img = ombiLinkEl.querySelector('img.service-icon.ombi'); expect(img).toBeTruthy(); expect(img.title).toBe('View on Ombi'); }); it('renders Sonarr icon link for administrator when arrType is sonarr and arrLink exists', () => { state.isAdmin = true; // Admin required for Sonarr link const dl = { title: 'Mandalorian S01E01', type: 'series', seriesName: 'The Mandalorian', arrType: 'sonarr', arrLink: 'https://sonarr.test/series/the-mandalorian' }; const card = createDownloadCard(dl); const arrLinkEl = card.querySelector('.download-series a'); expect(arrLinkEl).toBeTruthy(); expect(arrLinkEl.href).toBe('https://sonarr.test/series/the-mandalorian'); const img = arrLinkEl.querySelector('img.service-icon.sonarr'); expect(img).toBeTruthy(); expect(img.title).toBe('Sonarr'); }); it('renders Radarr icon link for administrator when arrType is radarr and arrLink exists', () => { state.isAdmin = true; // Admin required for Radarr link const dl = { title: 'Blade Runner 2049', type: 'movie', movieName: 'Blade Runner 2049', arrType: 'radarr', arrLink: 'https://radarr.test/movie/blade-runner-2049' }; const card = createDownloadCard(dl); const arrLinkEl = card.querySelector('.download-movie a'); expect(arrLinkEl).toBeTruthy(); expect(arrLinkEl.href).toBe('https://radarr.test/movie/blade-runner-2049'); const img = arrLinkEl.querySelector('img.service-icon.radarr'); expect(img).toBeTruthy(); expect(img.title).toBe('Radarr'); }); it('does not render Sonarr/Radarr links if the user is a non-admin', () => { state.isAdmin = false; // Non-admin const dl = { title: 'Mandalorian S01E01', type: 'series', seriesName: 'The Mandalorian', arrType: 'sonarr', arrLink: 'https://sonarr.test/series/the-mandalorian' }; const card = createDownloadCard(dl); const arrLinkEl = card.querySelector('.download-series a'); expect(arrLinkEl).toBeNull(); // Admin gate successfully hides Sonarr icon }); }); });