diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/tests/ui-core.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/tests/ui-core.test.mjs new file mode 100644 index 00000000..7ba51456 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/tests/ui-core.test.mjs @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import vm from "node:vm"; + +class ClassList { + constructor() { this.values = new Set(); } + add(...names) { names.forEach(name => this.values.add(name)); } + remove(...names) { names.forEach(name => this.values.delete(name)); } + contains(name) { return this.values.has(name); } + toggle(name, force) { + const enabled = force === undefined ? !this.contains(name) : force; + if (enabled) this.add(name); else this.remove(name); + return enabled; + } +} + +class Element { + constructor(tagName, document) { + this.tagName = tagName.toUpperCase(); + this.ownerDocument = document; + this.children = []; + this.dataset = {}; + this.attributes = new Map(); + this.classList = new ClassList(); + this.listeners = new Map(); + this.style = {}; + this.options = []; + this.value = ""; + this.textContent = ""; + } + set id(value) { this._id = value; if (value) this.ownerDocument.elements.set(value, this); } + get id() { return this._id || ""; } + set className(value) { this.classList = new ClassList(); value.split(/\s+/).filter(Boolean).forEach(name => this.classList.add(name)); } + set innerHTML(value) { + this._innerHTML = value; + if (value.includes('value="confirm"')) { + const title = new Element("h2", this.ownerDocument); title.id = "ui-confirm-title"; + const message = new Element("p", this.ownerDocument); message.id = "ui-confirm-message"; + const confirm = new Element("button", this.ownerDocument); confirm.value = "confirm"; + this.append(title, message, confirm); + this._confirmButton = confirm; + } + } + get innerHTML() { return this._innerHTML || ""; } + appendChild(child) { this.children.push(child); child.parentElement = this; return child; } + append(...children) { children.forEach(child => this.appendChild(child)); } + insertBefore(child) { return this.appendChild(child); } + remove() { if (this.parentElement) this.parentElement.children = this.parentElement.children.filter(child => child !== this); } + replaceChildren(...children) { this.children = []; this.options = []; this.append(...children); } + setAttribute(name, value) { this.attributes.set(name, String(value)); } + getAttribute(name) { return this.attributes.get(name); } + addEventListener(type, listener) { this.listeners.set(type, listener); } + dispatch(type, event = {}) { this.listeners.get(type)?.({ target: this, preventDefault() {}, ...event }); } + querySelector(selector) { + if (selector === '[value="confirm"]') return this._confirmButton || null; + return null; + } + querySelectorAll(selector) { + if (selector === ".tab[data-tab]") return this.children.filter(child => child.dataset.tab); + if (selector === ".sub-tab[data-subtab]") return this.children.filter(child => child.dataset.subtab); + if (selector === '[role="tab"]') return this.children.filter(child => child.getAttribute("role") === "tab"); + return []; + } + add(option) { this.options.push(option); if (!this.value) this.value = option.value; } + showModal() { this.open = true; } + close(value) { this.returnValue = value; this.open = false; this.dispatch("close"); } + focus() { this.focused = true; } + click() { this.clicked = true; } +} + +class DocumentFixture { + constructor() { + this.readyState = "loading"; + this.elements = new Map(); + this.body = new Element("body", this); + } + createElement(tagName) { return new Element(tagName, this); } + getElementById(id) { return this.elements.get(id) || null; } + querySelector() { return null; } + querySelectorAll() { return []; } + addEventListener() {} +} + +const document = new DocumentFixture(); +const storage = new Map(); +const localStorage = { + getItem(key) { return storage.has(key) ? storage.get(key) : null; }, + setItem(key, value) { storage.set(key, String(value)); }, +}; +const window = { document, localStorage, addEventListener() {} }; +const context = vm.createContext({ + window, document, localStorage, + Option: class Option { constructor(label, value) { this.label = label; this.value = value; } }, + MutationObserver: class MutationObserver { observe() {} }, + requestAnimationFrame(callback) { callback(); }, + setTimeout() { return 1; }, + clearTimeout() {}, + console, +}); + +const source = await readFile(new URL("../ui-core.js", import.meta.url), "utf8"); +new vm.Script(source, { filename: "ui-core.js" }).runInContext(context); +const ui = window.trxUi; + +const toast = ui.notify("Saved", { kind: "success" }); +assert.equal(toast.getAttribute("role"), "status"); +assert.equal(toast.classList.contains("toast-success"), true); +assert.equal(document.getElementById("toast-region").children.length, 1); + +const confirmation = ui.confirm({ title: "Delete?", message: "Permanent", confirmLabel: "Delete" }); +const dialog = document.getElementById("ui-confirm-dialog"); +assert.equal(dialog.open, true); +assert.equal(document.getElementById("ui-confirm-title").textContent, "Delete?"); +dialog.close("confirm"); +assert.equal(await confirmation, true); + +const tabBar = new Element("div", document); +const firstTab = new Element("button", document); firstTab.dataset.tab = "main"; firstTab.classList.add("active"); +const secondTab = new Element("button", document); secondTab.dataset.tab = "map"; +tabBar.append(firstTab, secondTab); +const mainPanel = new Element("section", document); mainPanel.id = "tab-main"; +const mapPanel = new Element("section", document); mapPanel.id = "tab-map"; +ui.prepareTabList(tabBar, "primary"); +assert.equal(firstTab.getAttribute("aria-selected"), "true"); +tabBar.dispatch("keydown", { target: firstTab, key: "ArrowRight" }); +assert.equal(secondTab.focused, true); +assert.equal(secondTab.clicked, true); + +localStorage.setItem("trxOperatorLayout:rig-a", "broadcast"); +ui.setActiveRig("rig-a"); +assert.equal(document.body.dataset.operatorLayout, "compact"); +ui.setLayoutCapabilities({ broadcast: true }); +ui.setActiveRig("rig-a"); +assert.equal(document.body.dataset.operatorLayout, "broadcast"); +ui.setActiveRig("rig-b"); +ui.applyLayout("digital"); +assert.equal(document.body.dataset.operatorLayout, "compact"); +assert.equal(localStorage.getItem("trxOperatorLayout:rig-b"), "compact"); + +console.log("ui-core component tests passed");