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>
121 lines
5.8 KiB
JavaScript
121 lines
5.8 KiB
JavaScript
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] });
|
||
});
|