"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const {loadApp} = require("./harness"); const {makeServer, M3U_ID, XMLTV_ID} = require("./fixture"); 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; } function inlineHandlerAttributes(root) { const found = []; for (const element of root.querySelectorAll("*")) { for (const attr of element.attributes) { if (/^on/i.test(attr.name)) { found.push(element.tagName + "@" + attr.name); } } } return found; } function change(window, element) { element.dispatchEvent(new window.Event("change", {bubbles: true})); } test("rendered pages carry no inline event handler attributes", () => { const app = loadApp({server: makeServer({settings: {"authentication.web": true}})}); app.window.createLayout(); for (const key of ["playlist", "filter", "xmltv", "mapping", "users", "settings", "log"]) { clickMenu(app.document, key); assert.deepEqual(inlineHandlerAttributes(app.document.body), [], "no inline handlers after opening " + key); } clickMenu(app.document, "playlist"); app.document.getElementById(M3U_ID).click(); assert.deepEqual(inlineHandlerAttributes(app.document.getElementById("popup-custom")), [], "no inline handlers in the m3u popup"); }); test("menu items open their content through listeners", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); clickMenu(app.document, "xmltv"); const rows = app.document.querySelectorAll("#content_table tr"); assert.equal(rows.length, 2); assert.equal(rows[1].id, XMLTV_ID); assert.equal(app.document.body.getAttribute("data-active-menu"), "xmltv"); }); test("playlist rows open the edit popup and the New button walks the type chooser", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); clickMenu(app.document, "playlist"); app.document.getElementById(M3U_ID).click(); const popup = app.document.getElementById("popup-custom"); assert.equal(popup.querySelector("h3").textContent, "M3U"); assert.equal(popup.querySelector('input[name="name"]').value, "Provider A"); assert.equal(popup.querySelector('select[name="tuner"]').value, "2"); assert.ok(popup.querySelector('input[name="delete"]'), "existing playlist offers delete"); app.document.querySelector('#content-interaction input[name="playlist"]').click(); assert.equal(popup.querySelector("h3").textContent, "PLAYLIST TYPE"); const type = popup.querySelector("select#type"); assert.deepEqual(Array.from(type.options).map(o => o.value), ["m3u", "hdhr"]); type.value = "hdhr"; change(app.window, type); app.document.getElementById("next").click(); assert.equal(popup.querySelector("h3").textContent, "HDHR"); assert.equal(popup.querySelector('input[name="back"]') != null, true, "new entry offers back instead of delete"); assert.equal(popup.querySelector('input[name="delete"]'), null); popup.querySelector('input[name="back"]').click(); assert.equal(popup.querySelector("h3").textContent, "PLAYLIST TYPE"); app.document.getElementById("next").click(); assert.equal(popup.querySelector("h3").textContent, "M3U"); popup.querySelector('input[name="cancel"]').click(); assert.equal(app.document.getElementById("popup").className, "none"); }); test("filter New button offers group-title and custom filters", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); clickMenu(app.document, "filter"); app.document.querySelector('#content-interaction input[name="filter"]').click(); const popup = app.document.getElementById("popup-custom"); assert.equal(popup.querySelector("h3").textContent, "FILTER"); const type = popup.querySelector('select[name="type"]'); assert.equal(type.value, "group-title"); type.value = "custom-filter"; change(app.window, type); app.document.getElementById("next").click(); assert.equal(popup.querySelector("h3").textContent, "CUSTOM"); assert.equal(popup.querySelector('input[name="type"]').value, "custom-filter"); assert.ok(popup.querySelector('input[name="filter"]'), "custom filter rule input"); }); test("mapping popup marks edited controls and applies them on Done", () => { const server = makeServer(); const app = loadApp({server}); app.window.createLayout(); clickMenu(app.document, "mapping"); const id = M3U_ID + "-1"; app.document.getElementById(id).querySelector(".mapping-edit-button").click(); const popup = app.document.getElementById("popup-custom"); const name = popup.querySelector('input[name="x-name"]'); assert.equal(name.className, ""); name.value = "Renamed"; change(app.window, name); assert.equal(name.className, "changed"); const group = popup.querySelector('input[name="x-group-title"]'); group.value = "Docs"; change(app.window, group); popup.querySelector('input[name="save"]').click(); const row = app.document.getElementById(id); assert.equal(row.children[3].textContent, "Renamed"); assert.equal(row.children[5].textContent, "Docs"); assert.equal(server.xepg.epgMapping[id]["x-name"], "Renamed"); assert.equal(server.xepg.epgMapping[id]["x-group-title"], "Docs"); assert.equal(app.document.getElementById("popup").className, "none"); }); test("changing the XMLTV file in the mapping popup rebuilds the channel list", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); clickMenu(app.document, "mapping"); app.document.getElementById(M3U_ID + "-1").querySelector(".mapping-edit-button").click(); const popup = app.document.getElementById("popup-custom"); const file = popup.querySelector("#popup-xmltv"); file.value = "xTeVe Dummy"; change(app.window, file); const mapping = popup.querySelector("#popup-mapping"); const values = Array.from(mapping.options).map(o => o.value); assert.ok(values.includes("30_Minutes"), "dummy programs listed"); assert.equal(mapping.className, "changed"); assert.equal(popup.querySelector("#update-icon").className, "changed"); mapping.value = "60_Minutes"; change(app.window, mapping); assert.equal(popup.querySelector("#active").checked, true); }); test("mapping table header sorts by channel name on click", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); clickMenu(app.document, "mapping"); const header = app.document.querySelector("#content_table tr"); const nameHeader = header.children[3]; assert.equal(nameHeader.className, "pointer"); nameHeader.click(); const names = Array.from(app.document.querySelectorAll("#content_table tr")).slice(1).map(tr => tr.children[3].textContent); assert.deepEqual(names, names.slice().sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)); }); test("bulk header checkbox selects every visible channel", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); clickMenu(app.document, "mapping"); app.document.querySelector('#content-interaction input[value="Bulk Edit"]').click(); const head = app.document.querySelector("#content_table tr input.bulk"); head.click(); const boxes = Array.from(app.document.querySelectorAll("#content_table tr")).slice(1).map(tr => tr.querySelector("input.bulk")); assert.equal(boxes.length, 5); assert.ok(boxes.every(b => b.checked), "every row checkbox is ticked"); assert.ok(boxes.every(b => b.className == "bulk showBulk")); }); test("filter New button offers a regular-expression filter", () => { const app = loadApp({server: makeServer()}); app.window.createLayout(); clickMenu(app.document, "filter"); app.document.querySelector('#content-interaction input[name="filter"]').click(); const popup = app.document.getElementById("popup-custom"); const type = popup.querySelector('select[name="type"]'); const values = Array.from(type.options).map((o) => o.value); assert.deepEqual(values, ["group-title", "custom-filter", "regex-filter"]); type.value = "regex-filter"; change(app.window, type); app.document.getElementById("next").click(); assert.equal(popup.querySelector("h3").textContent, "REGULAR EXPRESSION"); assert.equal(popup.querySelector('input[name="type"]').value, "regex-filter"); assert.ok(popup.querySelector('input[name="filter"]'), "regex rule input"); assert.ok(popup.querySelector('input[name="caseSensitive"]'), "case sensitive checkbox"); }); test("playlist popup offers a per-playlist buffer choice and keeps the tuner editable", () => { const server = makeServer(); const app = loadApp({server}); app.window.createLayout(); clickMenu(app.document, "playlist"); const row = app.document.querySelector("#content_table tbody tr, #content_table tr[id]"); assert.ok(row, "a playlist row"); row.click(); const popup = app.document.getElementById("popup-custom"); const buffer = popup.querySelector('select[name="buffer"]'); assert.ok(buffer, "buffer select"); assert.deepEqual(Array.from(buffer.options).map((o) => o.value), ["default", "-", "xteve", "ffmpeg", "vlc"]); assert.equal(buffer.value, "default"); assert.ok(popup.querySelector('select[name="tuner"]'), "tuner select shown while a buffer is active"); });