chore: bump version to 1.7.22 and update CHANGELOG, tests and docs
Docs Check / Markdown lint (push) Successful in 1m14s
Build and Push Docker Image / build (push) Successful in 1m52s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m55s
CI / Security audit (push) Successful in 2m25s
Docs Check / Mermaid diagram parse check (push) Successful in 3m6s
CI / Swagger Validation & Coverage (push) Successful in 3m22s
CI / Tests & coverage (push) Successful in 3m45s

This commit is contained in:
2026-05-27 17:42:41 +01:00
parent d2ac7731ca
commit 95bd703b26
16 changed files with 664 additions and 65 deletions
+178
View File
@@ -0,0 +1,178 @@
// Copyright (c) 2026 Gordon Bolton. MIT License.
/**
* @vitest-environment jsdom
* Tests for client/src/ui/requests.js
*
* Verifies requests dashboard rendering, tooltips, dates, and deep links.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderRequests } from '../../../client/src/ui/requests.js';
import { state } from '../../../client/src/state.js';
vi.mock('../../../client/src/state.js', () => {
return {
state: {
ombiRequests: { movie: [], tv: [] },
selectedRequestTypes: ['movie', 'tv'],
selectedRequestStatuses: ['pending', 'approved', 'available', 'denied'],
requestSortMode: 'requestedDate_desc',
requestSearchQuery: '',
ombiBaseUrl: 'https://ombi.test',
isAdmin: false
}
};
});
describe('requests rendering', () => {
let requestsList, noRequests;
beforeEach(() => {
vi.clearAllMocks();
document.body.innerHTML = `
<div id="requests-list"></div>
<div id="no-requests" style="display: none;"><p></p></div>
`;
requestsList = document.getElementById('requests-list');
noRequests = document.getElementById('no-requests');
state.ombiRequests = { movie: [], tv: [] };
state.isAdmin = false;
state.ombiBaseUrl = 'https://ombi.test';
});
it('renders "No requests found." when request arrays are empty', () => {
renderRequests();
expect(requestsList.childNodes.length).toBe(0);
expect(noRequests.style.display).toBe('block');
expect(noRequests.querySelector('p').textContent).toBe('No requests found.');
});
it('renders request card with correctly formatted date, media type, and requester', () => {
state.ombiRequests = {
movie: [
{
id: 101,
title: 'Movie Test',
year: '2026',
requestedUser: { alias: 'john_doe' },
requestedDate: '2026-05-27T10:15:30.000Z',
quality: '1080p',
theMovieDbId: 555,
requested: true
}
],
tv: []
};
renderRequests();
expect(requestsList.childNodes.length).toBe(1);
const card = requestsList.childNodes[0];
expect(card.querySelector('.request-title').textContent).toBe('Movie Test');
expect(card.querySelector('.request-year').textContent).toBe('2026');
expect(card.querySelector('.request-user').textContent).toBe('Requested by: john_doe');
// Check formatted date
const dateEl = card.querySelector('.request-date');
expect(dateEl).toBeTruthy();
expect(dateEl.textContent).toContain('Date: 2026-05-27');
// Check view in Ombi link
const ombiLink = card.querySelector('.ombi-link');
expect(ombiLink).toBeTruthy();
expect(ombiLink.href).toBe('https://ombi.test/details/movie/555');
});
it('renders "Unknown (Ombi)" with tooltip when requester is missing', () => {
state.ombiRequests = {
movie: [],
tv: [
{
id: 201,
title: 'TV Test No User',
requestedDate: '2026-05-27T12:00:00.000Z',
requested: true
}
]
};
renderRequests();
expect(requestsList.childNodes.length).toBe(1);
const card = requestsList.childNodes[0];
const userEl = card.querySelector('.request-user');
expect(userEl).toBeTruthy();
expect(userEl.textContent).toBe('Requested by: Unknown (Ombi)');
expect(userEl.title).toBe('No user information received from Ombi');
expect(userEl.style.textDecoration).toBe('underline dotted');
});
it('does NOT render Sonarr/Radarr deep links for non-admin users', () => {
state.isAdmin = false;
state.ombiRequests = {
movie: [
{
id: 101,
title: 'Movie Test',
theMovieDbId: 555,
arrLink: 'http://radarr:7878/movie/slug',
arrType: 'radarr',
requested: true
}
],
tv: []
};
renderRequests();
const card = requestsList.childNodes[0];
expect(card.querySelector('.radarr-link')).toBeNull();
});
it('renders Sonarr/Radarr deep links next to Ombi link for administrators', () => {
state.isAdmin = true;
state.ombiRequests = {
movie: [
{
id: 101,
title: 'Movie Test',
theMovieDbId: 555,
arrLink: 'http://radarr:7878/movie/slug',
arrType: 'radarr',
requested: true
}
],
tv: [
{
id: 202,
title: 'TV Show Test',
theMovieDbId: 666,
arrLink: 'http://sonarr:8989/series/slug',
arrType: 'sonarr',
requested: true
}
]
};
renderRequests();
expect(requestsList.childNodes.length).toBe(2);
// Check Radarr link
const movieCard = requestsList.childNodes[0];
const radarrLink = movieCard.querySelector('.radarr-link');
expect(radarrLink).toBeTruthy();
expect(radarrLink.href).toBe('http://radarr:7878/movie/slug');
expect(radarrLink.title).toBe('View in Radarr');
// Check Sonarr link
const tvCard = requestsList.childNodes[1];
const sonarrLink = tvCard.querySelector('.sonarr-link');
expect(sonarrLink).toBeTruthy();
expect(sonarrLink.href).toBe('http://sonarr:8989/series/slug');
expect(sonarrLink.title).toBe('View in Sonarr');
});
});
+94
View File
@@ -0,0 +1,94 @@
// Copyright (c) 2026 Gordon Bolton. MIT License.
/**
* @vitest-environment jsdom
* Tests for client/src/ui/theme.js
*
* Verifies DOM actions for theme switcher button clicks, attributes, and storage calls.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { initThemeSwitcher, setTheme } from '../../../client/src/ui/theme.js';
import * as storage from '../../../client/src/utils/storage.js';
vi.mock('../../../client/src/utils/storage.js', () => {
let store = {};
return {
getTheme: vi.fn(() => store.theme || 'light'),
saveTheme: vi.fn((theme) => { store.theme = theme; })
};
});
describe('theme switcher', () => {
let lightBtn, darkBtn, monoBtn;
beforeEach(() => {
vi.clearAllMocks();
document.documentElement.removeAttribute('data-theme');
// Create mock theme buttons
document.body.innerHTML = `
<div class="theme-switcher">
<button class="theme-btn" data-theme="light">Light</button>
<button class="theme-btn" data-theme="dark">Dark</button>
<button class="theme-btn" data-theme="mono">Mono</button>
</div>
`;
lightBtn = document.querySelector('[data-theme="light"]');
darkBtn = document.querySelector('[data-theme="dark"]');
monoBtn = document.querySelector('[data-theme="mono"]');
});
it('initThemeSwitcher sets active class based on saved theme on load', () => {
vi.spyOn(storage, 'getTheme').mockReturnValue('dark');
initThemeSwitcher();
expect(storage.getTheme).toHaveBeenCalled();
expect(darkBtn.classList.contains('active')).toBe(true);
expect(lightBtn.classList.contains('active')).toBe(false);
expect(monoBtn.classList.contains('active')).toBe(false);
});
it('initThemeSwitcher defaults to light theme if no theme is saved', () => {
vi.spyOn(storage, 'getTheme').mockReturnValue(null);
initThemeSwitcher();
expect(lightBtn.classList.contains('active')).toBe(true);
expect(darkBtn.classList.contains('active')).toBe(false);
});
it('clicking theme button switches the document theme and persists choice', () => {
initThemeSwitcher();
// Initial active button should be light
expect(lightBtn.classList.contains('active')).toBe(true);
// Click Dark
darkBtn.click();
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
expect(storage.saveTheme).toHaveBeenCalledWith('dark');
expect(darkBtn.classList.contains('active')).toBe(true);
expect(lightBtn.classList.contains('active')).toBe(false);
// Click Mono
monoBtn.click();
expect(document.documentElement.getAttribute('data-theme')).toBe('mono');
expect(storage.saveTheme).toHaveBeenCalledWith('mono');
expect(monoBtn.classList.contains('active')).toBe(true);
expect(darkBtn.classList.contains('active')).toBe(false);
});
it('setTheme directly sets document attribute and updates button classes if present', () => {
initThemeSwitcher(); // binds buttons
setTheme('mono');
expect(document.documentElement.getAttribute('data-theme')).toBe('mono');
expect(storage.saveTheme).toHaveBeenCalledWith('mono');
expect(monoBtn.classList.contains('active')).toBe(true);
expect(lightBtn.classList.contains('active')).toBe(false);
});
});
+51
View File
@@ -79,6 +79,57 @@ describe('ombiHelpers', () => {
};
expect(extractRequestedUser(req)).toBe('');
});
it('returns userName from nested user object', () => {
const req = { user: { userName: 'user_val' } };
expect(extractRequestedUser(req)).toBe('user_val');
});
it('returns alias from nested requestedBy object', () => {
const req = { requestedBy: { alias: 'req_alias' } };
expect(extractRequestedUser(req)).toBe('req_alias');
});
it('returns normalizedUserName from nested ombiUser object', () => {
const req = { ombiUser: { normalizedUserName: 'norm_ombi' } };
expect(extractRequestedUser(req)).toBe('norm_ombi');
});
it('returns userAlias from nested requestedByUser object', () => {
const req = { requestedByUser: { userAlias: 'alias_user' } };
expect(extractRequestedUser(req)).toBe('alias_user');
});
it('returns username from a string source value', () => {
const req = { requestedBy: 'direct_string' };
expect(extractRequestedUser(req)).toBe('direct_string');
});
it('returns username from root fallbacks (requestedByUsername, requester, requestedByEmail)', () => {
expect(extractRequestedUser({ requestedByUsername: 'user_uname' })).toBe('user_uname');
expect(extractRequestedUser({ requester: 'req_val' })).toBe('req_val');
expect(extractRequestedUser({ requestedByEmail: 'test@email.com' })).toBe('test@email.com');
});
it('recursively extracts user from seasons array requests', () => {
const req = {
seasons: [
{},
{ requestedUser: { alias: 'season_user' } }
]
};
expect(extractRequestedUser(req)).toBe('season_user');
});
it('recursively extracts user from childRequests array', () => {
const req = {
childRequests: [
{},
{ user: { userName: 'child_user' } }
]
};
expect(extractRequestedUser(req)).toBe('child_user');
});
});
describe('filterRequestsByUser', () => {