"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"});
});