test(ui): cover shared component behavior
This commit is contained in:
@@ -0,0 +1,144 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// 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");
|
||||||
Reference in New Issue
Block a user