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, '..'); function readPublicScript(relativePath, exposeGlobal = '') { const source = fs.readFileSync(path.join(root, 'public', relativePath), 'utf8'); return exposeGlobal ? `${source}\nwindow.${exposeGlobal} = ${exposeGlobal};` : source; } function createSettingsDom() { const dom = new JSDOM(`
`, { url: 'http://nodecast.test/', runScripts: 'outside-only', pretendToBeVisual: true }); const { window } = dom; window.console = console; window.eval(readPublicScript('js/services/liveAnalysisPresentation.js')); window.eval(readPublicScript('js/pages/Settings.js', 'SettingsPage')); return window; } test('Channel Analysis keeps progress visible for a live source sync started outside the current UI action', async () => { const window = createSettingsDom(); let sourceStatusRead = false; window.API = { live: { getProjectionStatus: async () => ({ status: 'success', phase: 'complete', projectedCount: 42, processed: 42, total: 42 }) }, sources: { getStatus: async () => { sourceStatusRead = true; return [{ source_id: 1, type: 'all', status: 'syncing', last_sync: Date.now(), error: null }]; } } }; const settingsPage = Object.create(window.SettingsPage.prototype); settingsPage.app = { player: { settings: { languageDetectionMode: 'algo' } }, channelList: { loadChannels() {} } }; settingsPage.layaStatus = {}; settingsPage.currentLiveAnalysisAction = ''; try { await settingsPage.loadLiveAnalysisStatus(); assert.equal(sourceStatusRead, true, 'Settings should consult /api/sources/status when projection status is not busy'); assert.equal(window.document.getElementById('live-analysis-progress').hidden, false); assert.equal(window.document.getElementById('live-analysis-status').textContent, 'Refreshing IPTV sources…'); assert.equal(window.document.getElementById('live-analysis-progress-text').textContent, 'In progress — this step may take several minutes.'); assert.equal(window.document.getElementById('live-analysis-progress-track').classList.contains('indeterminate'), true); const rebuildButton = window.document.getElementById('live-rebuild-catalog-button'); assert.equal(rebuildButton.disabled, true); assert.equal(rebuildButton.textContent, 'Run full refresh'); } finally { window.close(); } }); test('busy projection status remains authoritative over a simultaneous source sync', async () => { const window = createSettingsDom(); window.API = { live: { getProjectionStatus: async () => ({ status: 'building', phase: 'laya_language', phaseProcessed: 1, phaseTotal: 2 }) }, sources: { getStatus: async () => { throw new Error('should not be called'); } } }; const settingsPage = Object.create(window.SettingsPage.prototype); settingsPage.app = { player: { settings: { languageDetectionMode: 'laya' } } }; settingsPage.layaStatus = {}; settingsPage.currentLiveAnalysisAction = ''; try { await settingsPage.loadLiveAnalysisStatus(); assert.equal(window.document.getElementById('live-analysis-status').textContent, 'Detecting stream languages…'); assert.equal(window.document.getElementById('live-rebuild-catalog-button').textContent, 'Refreshing…'); } finally { window.close(); } }); test('EPG-only source sync is ignored and source status failures preserve projection status', async () => { for (const getStatus of [ async () => [{ type: 'epg', status: 'syncing' }], async () => { throw new Error('source status unavailable'); } ]) { const window = createSettingsDom(); window.API = { live: { getProjectionStatus: async () => ({ status: 'success', phase: 'complete', projectedCount: 42 }) }, sources: { getStatus } }; const settingsPage = Object.create(window.SettingsPage.prototype); settingsPage.app = { player: { settings: { languageDetectionMode: 'algo' } } }; settingsPage.layaStatus = {}; settingsPage.currentLiveAnalysisAction = ''; try { const status = await settingsPage.loadLiveAnalysisStatus(); assert.equal(status.phase, 'complete'); assert.equal(window.document.getElementById('live-analysis-progress').hidden, true); assert.equal(window.document.getElementById('live-rebuild-catalog-button').disabled, false); } finally { window.close(); } } });