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(`
`, { url: 'http://nodecast.test/', runScripts: 'outside-only' }); dom.window.Icons = { series: 'guide', refresh: 'refresh', settings: 'edit', close: 'delete' }; 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] }); });