"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const vm = require("vm"); const {loadApp, readBundle} = require("./harness"); const {makeServer, M3U_ID} = require("./fixture"); function menuKeys(document) { return Array.from(document.querySelectorAll("#main-menu li")).map(li => li.getAttribute("data-menu")); } function clickMenu(document, key) { const item = document.querySelector('#main-menu li[data-menu="' + key + '"]'); assert.ok(item, "menu item " + key + " is rendered"); item.click(); return item; } test("bundle evaluates without throwing", () => { const app = loadApp(); assert.equal(typeof app.window.createLayout, "function"); assert.equal(typeof app.window.PageReady, "function"); assert.equal(app.evalIn("typeof Server"), "function"); assert.equal(app.evalIn("typeof WSClient"), "function"); assert.equal(app.evalIn("menuItems.length"), 8); }); test("bundle evaluates against a bare window without a document", () => { const context = vm.createContext({ window: {}, console, setTimeout, clearTimeout, setInterval, clearInterval, WebSocket: function() {}, }); context.window = context; assert.doesNotThrow(() => vm.runInContext(readBundle(), context, {filename: "app.js"})); }); test("createLayout hides users and logout while web authentication is off", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); const keys = menuKeys(app.document); assert.deepEqual(keys, ["playlist", "filter", "xmltv", "mapping", "settings", "log"]); assert.equal(app.document.getElementById("version").textContent, "2.5.0"); assert.equal(app.document.querySelectorAll("#status-cards .status-card").length, 6); }); test("createLayout shows users and logout once web authentication is on", () => { const app = loadApp({server: makeServer({settings: {"authentication.web": true}})}); app.window.createLayout(); const keys = menuKeys(app.document); assert.ok(keys.includes("users"), "users item present"); assert.ok(keys.includes("logout"), "logout item present"); assert.equal(keys.length, 8); }); test("createLayout hides mapping and xmltv unless the EPG source is XEPG", () => { const app = loadApp({server: makeServer({clientInfo: {epgSource: "PMS"}, settings: {epgSource: "PMS"}})}); app.window.createLayout(); const keys = menuKeys(app.document); assert.ok(!keys.includes("mapping"), "mapping hidden"); assert.ok(!keys.includes("xmltv"), "xmltv hidden"); assert.ok(keys.includes("playlist"), "playlist still present"); }); test("createLayout opens the first menu item when nothing is active", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); const active = app.document.querySelector("#main-menu li.menu-active"); assert.ok(active, "an item is marked active"); assert.equal(active.getAttribute("data-menu"), "playlist"); assert.equal(app.document.body.getAttribute("data-active-menu"), "playlist"); assert.equal(app.document.querySelectorAll("#content_table tr").length, 2, "header plus one playlist row"); }); test("mapping menu renders one row per channel with channel names as text", () => { const server = makeServer(); const app = loadApp({server}); app.window.createLayout(); clickMenu(app.document, "mapping"); const table = app.document.getElementById("content_table"); assert.ok(table, "mapping table rendered"); const channelCount = Object.keys(server.xepg.epgMapping).length; const rows = Array.from(table.querySelectorAll("tr")).slice(1); assert.equal(rows.length, channelCount); const ids = rows.map(tr => tr.id).sort(); assert.deepEqual(ids, Object.keys(server.xepg.epgMapping).sort()); const hostile = app.document.getElementById(M3U_ID + "-2"); assert.ok(hostile, "row for the hostile channel name exists"); assert.equal(hostile.querySelector("b"), null, "channel name is not parsed as markup"); const names = rows.map(tr => tr.children[3].textContent); assert.ok(names.includes("x"), "channel name appears literally"); assert.ok(names.includes("News 24")); const playlists = rows.map(tr => tr.children[4].textContent); assert.ok(playlists.every(p => p === "Provider A"), "playlist name resolved from the file id"); const guides = rows.map(tr => tr.children[6].textContent).sort(); assert.deepEqual(guides, ["-", "Guide A", "Guide A", "Guide A", "xTeVe Dummy"]); const inactive = app.document.getElementById(M3U_ID + "-5"); assert.equal(inactive.className, "notActiveEPG"); assert.equal(app.document.getElementById(M3U_ID + "-1").className, "activeEPG"); const edit = hostile.querySelector(".mapping-edit-button"); assert.ok(edit, "edit button rendered"); assert.equal(edit.getAttribute("aria-label"), "Edit x"); }); test("mapping edit button opens the popup for its channel", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); clickMenu(app.document, "mapping"); const edit = app.document.getElementById(M3U_ID + "-3").querySelector(".mapping-edit-button"); edit.click(); const popup = app.document.getElementById("popup-custom"); assert.equal(popup.querySelector("h3").textContent, "MAPPING"); assert.equal(popup.querySelector('input[name="x-name"]').value, "Sports HD"); assert.equal(popup.querySelector('select[name="x-xmltv-file"]').value, "X-1-def.xml"); assert.equal(popup.querySelector('select[name="x-mapping"]').value, "ch3.example"); }); function expectedSettingsKeys(app, webAuth) { const all = app.evalIn("settingsCategory.map(function(c) { return c.settingsKeys }).join(',')").split(","); if (webAuth) { return all; } return all.filter(key => !["authentication.pms", "authentication.m3u", "authentication.xml", "authentication.api"].includes(key)); } function assertSettingsControls(app, keys) { const panel = app.document.getElementById("content_settings"); assert.ok(panel, "settings panel rendered"); const controls = Array.from(panel.querySelectorAll("input[name], select[name]")); assert.equal(controls.length, keys.length, "one control per settings key"); for (const key of keys) { const matches = controls.filter(c => c.getAttribute("name") === key); assert.equal(matches.length, 1, "exactly one control for " + key); } return controls; } test("settings panel renders one input per settings key", () => { const server = makeServer(); const app = loadApp({server}); app.window.createLayout(); clickMenu(app.document, "settings"); const keys = expectedSettingsKeys(app, false); const controls = assertSettingsControls(app, keys); const byName = Object.fromEntries(controls.map(c => [c.getAttribute("name"), c])); assert.equal(byName["tuner"].tagName, "SELECT"); assert.equal(byName["tuner"].value, "1"); assert.equal(byName["buffer"].value, "ffmpeg"); assert.equal(byName["epgSource"].value, "XEPG"); assert.equal(byName["plex.token"].type, "password"); assert.equal(byName["plex.token"].value, "********"); assert.equal(byName["update"].value, "0000,1200"); assert.equal(byName["files.update"].type, "checkbox"); assert.equal(byName["files.update"].checked, true); assert.equal(byName["cache.images"].checked, false); assert.equal(byName["authentication.web"].checked, false); }); test("settings panel includes the per-service authentication keys when web auth is on", () => { const app = loadApp({server: makeServer({settings: {"authentication.web": true, "authentication.api": true}})}); app.window.createLayout(); clickMenu(app.document, "settings"); const keys = expectedSettingsKeys(app, true); const controls = assertSettingsControls(app, keys); const api = controls.find(c => c.getAttribute("name") === "authentication.api"); assert.equal(api.checked, true); }); test("saveSettings sends only the changed controls", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); clickMenu(app.document, "settings"); const tuner = app.document.querySelector('#content_settings select[name="tuner"]'); tuner.value = "4"; tuner.dispatchEvent(new app.window.Event("change", {bubbles: true})); const agent = app.document.querySelector('#content_settings input[name="user.agent"]'); agent.value = "Custom"; agent.dispatchEvent(new app.window.Event("change", {bubbles: true})); app.window.saveSettings(); const socket = app.sockets[app.sockets.length - 1]; assert.ok(socket, "a websocket was opened"); socket.readyState = 1; socket.onopen(); assert.equal(socket.sent.length, 1); const payload = JSON.parse(socket.sent[0]); assert.equal(payload.cmd, "saveSettings"); assert.deepEqual(payload.settings, {"tuner": 4, "user.agent": "Custom"}); });