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 script = relativePath => fs.readFileSync(path.join(root, 'public', relativePath), 'utf8'); const wait = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)); function createChannelList() { const dom = new JSDOM(`
`, { url: 'http://nodecast.test/', runScripts: 'outside-only', pretendToBeVisual: true }); const { window } = dom; window.IntersectionObserver = class { observe() {} disconnect() {} }; window.eval(script('js/icons.js')); window.eval(script('js/services/channelLanguageDetection.js')); window.eval(script('js/services/channelLanguageFusion.js')); window.eval(script('js/services/channelVariantGrouping.js')); window.eval(script('js/services/layaStatusPresentation.js')); window.eval(script('js/services/liveChannelProjection.js')); window.eval(`${script('js/components/ChannelList.js')}\nwindow.ChannelList = ChannelList;`); window.app = { player: { settings: { liveChannelListMode: 'flat', languageDetectionMode: 'algo' } }, epgGuide: { getCurrentProgram: () => null, loadError: null } }; return { dom, window, channelList: new window.ChannelList() }; } function projectedResponse() { const channel = (id, name, language) => ({ id: `${id}:${language}`, logicalChannelId: id, displayName: name, language: { code: language, short: language.toUpperCase(), label: language === 'fr' ? 'French' : 'English', confidence: 95 }, selectedVariantId: `1:${id}-${language}`, variants: [{ id: `${id}-${language}`, sourceId: 1, name: `${name} ${language.toUpperCase()}`, categoryName: 'Sports', isHidden: false }] }); return { projection: { stale: false }, filters: { availableLanguages: [ { code: 'en', short: 'EN', label: 'English' }, { code: 'fr', short: 'FR', label: 'French' } ] }, channels: [ channel('bein-1', 'beIN Sports 1', 'fr'), channel('bein-1', 'beIN Sports 1', 'en'), channel('news', 'World News', 'en') ] }; } test('typing filters a loaded projection locally with flexible matching and no API request', async () => { const { dom, window, channelList } = createChannelList(); let calls = 0; window.API = { live: { getChannels: async options => { calls++; assert.equal(options.q, ''); assert.deepEqual([...options.languages], []); return projectedResponse(); } } }; await channelList.loadProjectedChannels(); channelList.render(); calls = 0; channelList.searchInput.value = 'Bein Sport 1'; channelList.searchInput.dispatchEvent(new window.Event('input', { bubbles: true })); await wait(100); assert.equal(calls, 0); assert.equal(channelList.filteredChannels.length, 2); assert.ok(channelList.filteredChannels.every(channel => channel.displayName === 'beIN Sports 1')); assert.equal(channelList.listStatus.textContent, '2 channel entries shown'); dom.window.close(); }); test('language selection filters the complete loaded catalog immediately without API', async () => { const { dom, window, channelList } = createChannelList(); let calls = 0; window.API = { live: { getChannels: async () => { calls++; return projectedResponse(); } } }; await channelList.loadProjectedChannels(); channelList.render(); calls = 0; channelList.setLanguageSelection('fr'); assert.equal(calls, 0); assert.equal(channelList.filteredChannels.length, 1); assert.equal(channelList.filteredChannels[0].language.code, 'fr'); dom.window.close(); }); test('loading the projected channel list does not call Laya language detection during display', async () => { const { dom, window, channelList } = createChannelList(); window.app.player.settings.languageDetectionMode = 'laya'; let layaStatusCalls = 0; let layaBatchCalls = 0; window.API = { live: { getChannels: async () => projectedResponse() }, laya: { getStatus: async () => { layaStatusCalls += 1; return { modelPresent: true, runtimeAvailable: true }; }, detectLanguages: async () => { layaBatchCalls += 1; return { results: [] }; } } }; try { await channelList.loadAllChannels(); await wait(20); assert.equal(layaStatusCalls, 0); assert.equal(layaBatchCalls, 0); } finally { dom.window.close(); } }); test('Escape clears channel search while preserving language-panel Escape behavior', async () => { const { dom, window, channelList } = createChannelList(); window.API = { live: { getChannels: async () => projectedResponse() } }; await channelList.loadProjectedChannels(); channelList.searchInput.value = 'news'; window.document.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); assert.equal(channelList.searchInput.value, ''); assert.equal(channelList.filteredChannels.length, 3); dom.window.close(); });