Files
Nodecast/test/t24Frontend.test.js
blomios 803dac3788 feat(ui): add EPG source priority controls (#24)
Render active and inactive EPG priority lists, support move controls, and persist complete EPG source ordering from the settings UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-06 11:59:06 +02:00

121 lines
5.8 KiB
JavaScript
Raw Permalink 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.

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { JSDOM } = require('jsdom');
const root = path.resolve(__dirname, '..');
const readScript = file => fs.readFileSync(path.join(root, 'public', file), 'utf8');
function createManager() {
const dom = new JSDOM(`
<div id="epg-order-status"></div>
<div id="epg-active-list"></div>
<div id="epg-inactive-list"></div>
`, { url: 'http://nodecast.test/', runScripts: 'outside-only' });
dom.window.Icons = {
series: '<i>guide</i>', refresh: '<i>refresh</i>', settings: '<i>edit</i>', close: '<i>delete</i>'
};
dom.window.console.error = () => {};
dom.window.API = { sources: {} };
dom.window.eval(`${readScript('js/components/SourceManager.js')}\nwindow.SourceManager = SourceManager;`);
const manager = Object.create(dom.window.SourceManager.prototype);
manager.epgActiveList = dom.window.document.getElementById('epg-active-list');
manager.epgInactiveList = dom.window.document.getElementById('epg-inactive-list');
manager.epgOrderStatus = dom.window.document.getElementById('epg-order-status');
manager.isSavingEpgOrder = false;
manager.refreshSource = () => {};
manager.toggleEpgSource = () => {};
manager.showEditModal = () => {};
manager.deleteSource = () => {};
return { dom, manager };
}
const sources = () => [
{ id: 1, type: 'epg', name: 'Custom France', enabled: true, epgPriority: 0, language: 'fr', country: 'FR' },
{ id: 2, type: 'epg', name: 'EPG.PW US', enabled: true, epgPriority: 10, builtinKey: 'epgpw-us', language: 'en', country: 'US' },
{ id: 3, type: 'epg', name: 'Inactive custom', enabled: false, epgPriority: 20 }
];
test('EPG priority view renders ranked active sources and protected inactive/custom actions', () => {
const { manager } = createManager();
manager.renderEpgSources(sources());
const activeRows = manager.epgActiveList.querySelectorAll('.epg-source-item');
assert.equal(activeRows.length, 2);
assert.match(activeRows[0].textContent, /Priority 1/);
assert.match(activeRows[1].textContent, /Built-in/);
assert.equal(activeRows[1].querySelector('[data-action="edit"]'), null);
assert.equal(activeRows[1].querySelector('[data-action="delete"]'), null);
assert.equal(activeRows[0].querySelector('[data-action="move-up"]').disabled, true);
assert.equal(activeRows[0].querySelector('[data-action="move-down"]').getAttribute('aria-label'), 'Move Custom France down');
const inactive = manager.epgInactiveList.querySelector('.epg-source-item');
assert.doesNotMatch(inactive.textContent, /Priority/);
assert.ok(inactive.querySelector('[data-action="edit"]'));
assert.ok(inactive.querySelector('[data-action="delete"]'));
});
test('moving an active EPG source saves a complete order including inactive sources', async () => {
const { dom, manager } = createManager();
let orderedIds;
dom.window.API.sources.updateEpgOrder = async ids => {
orderedIds = [...ids];
return [sources()[1], sources()[0], sources()[2]];
};
manager.renderEpgSources(sources());
await manager.moveEpgSource(2, -1, 'move-up');
assert.deepEqual(orderedIds, [2, 1, 3]);
assert.match(manager.epgActiveList.querySelector('.epg-source-item').textContent, /EPG.PW US/);
assert.equal(manager.epgOrderStatus.textContent, 'EPG priority saved');
});
test('failed EPG priority save rolls the visual order back and announces the error', async () => {
const { dom, manager } = createManager();
dom.window.API.sources.updateEpgOrder = async () => { throw new Error('network'); };
manager.renderEpgSources(sources());
await manager.moveEpgSource(2, -1, 'move-up');
assert.match(manager.epgActiveList.querySelector('.epg-source-item').textContent, /Custom France/);
assert.equal(manager.epgOrderStatus.textContent, 'EPG priority couldn’t be saved. Try again.');
assert.equal(manager.epgOrderStatus.dataset.state, 'error');
});
test('enabling an EPG source appends it to the active priority list', async () => {
const { dom, manager } = createManager();
let orderedIds;
dom.window.API.sources.toggle = async () => ({ enabled: true });
dom.window.API.sources.getAll = async () => sources().map(source =>
source.id === 3 ? { ...source, enabled: true } : source
);
dom.window.API.sources.updateEpgOrder = async ids => {
orderedIds = [...ids];
return sources().map(source => source.id === 3 ? { ...source, enabled: true } : source);
};
await dom.window.SourceManager.prototype.toggleEpgSource.call(manager, 3);
assert.deepEqual(orderedIds, [1, 2, 3]);
assert.match(manager.epgActiveList.lastElementChild.textContent, /Inactive custom/);
});
test('empty active EPG state gives an actionable explanation', () => {
const { manager } = createManager();
manager.renderEpgSources(sources().map(source => ({ ...source, enabled: false })));
assert.match(manager.epgActiveList.textContent, /No active EPG sources/);
assert.match(manager.epgActiveList.textContent, /Enable a source to load program information/);
});
test('API client sends the complete EPG order to the dedicated endpoint', async () => {
const dom = new JSDOM('', { url: 'http://nodecast.test/', runScripts: 'outside-only' });
let request;
dom.window.fetch = async (url, options) => {
request = { url, options };
return { ok: true, headers: { get: () => 'application/json' }, json: async () => [] };
};
dom.window.eval(readScript('js/api.js'));
await dom.window.API.sources.updateEpgOrder([2, 1, 3]);
assert.equal(request.url, '/api/sources/epg/order');
assert.equal(request.options.method, 'PUT');
assert.deepEqual(JSON.parse(request.options.body), { orderedIds: [2, 1, 3] });
});