aec04474be
- Add tests/unit/utils/poller.test.js covering background polling lock, registry, error recovery, webhook bypasses, and global fallbacks - Add tests/integration/rateLimiter.test.js verifying 429 response rate-limiting in an isolated production environment - Add tests/integration/ombiDecoration.test.js covering deep links and admin role checks - Expand tests/frontend/ui/downloads.test.js covering createServiceIcons() and createClientLogo() fallbacks - Expand tests/integration/dashboard.test.js verifying SSE heartbeats, payload schema contract, and listener cleanup on client disconnect
237 lines
8.3 KiB
JavaScript
237 lines
8.3 KiB
JavaScript
// 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
|
|
});
|
|
});
|
|
});
|
|
|