fix(live): keep channel search responsive (#25)
Filter projected live channels locally in the browser and apply server-side search and language filters before costly enrichment work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
107
test/t25Frontend.test.js
Normal file
107
test/t25Frontend.test.js
Normal file
@ -0,0 +1,107 @@
|
||||
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(`<!doctype html>
|
||||
<input id="channel-search">
|
||||
<select id="source-select"><option value="">All Sources</option></select>
|
||||
<input id="show-hidden" type="checkbox">
|
||||
<button id="toggle-groups"></button>
|
||||
<div id="channel-language-filter"></div>
|
||||
<div id="channel-list-status"></div>
|
||||
<div id="context-menu"></div>
|
||||
<div id="channel-list"></div>
|
||||
`, { 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/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('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();
|
||||
});
|
||||
Reference in New Issue
Block a user