// Run with: node tests/acl_picker_dom.cjs. Tiny DOM stub for picker state/regression. "use strict"; const assert = require("node:assert/strict"); const fs = require("node:fs"); const vm = require("node:vm"); const path = require("node:path"); class DomEvent { constructor(type, options = {}) { this.type = type; this.bubbles = !!options.bubbles; this.target = null; } } class DomNode { constructor(tag) { this.tag = tag; this.className = ""; this.children = []; this.parent = null; this.dataset = {}; this.handlers = new Map(); this.hidden = false; this.disabled = false; this.checked = false; this.value = ""; this.type = ""; this._text = ""; } get textContent() { return this._text + this.children.map(c => c.textContent).join(""); } set textContent(text) { this.replaceChildren(); this._text = String(text); } append(...children) { for (const child of children) { child.parent = this; this.children.push(child); } } replaceChildren(...children) { for (const child of this.children) child.parent = null; this.children = []; this._text = ""; this.append(...children); } setAttribute() { } focus() { } addEventListener(type, handler) { if (!this.handlers.has(type)) this.handlers.set(type, []); this.handlers.get(type).push(handler); } dispatchEvent(event) { if (!event.target) event.target = this; for (const handler of this.handlers.get(event.type) || []) handler(event); if (event.bubbles && this.parent) this.parent.dispatchEvent(event); } click() { this.dispatchEvent(new DomEvent("click", {bubbles:true})); } hasClass(name) { return this.className.split(/\s+/).includes(name); } matches(selector) { return selector === 'input[type="checkbox"]' && this.tag === "input" && this.type === "checkbox"; } all() { return this.children.flatMap(child => [child, ...child.all()]); } querySelectorAll(selector) { const descendants = this.all(); if (selector === '.acl-picker-option input[type="checkbox"]:checked') return descendants.filter(n => n.tag === "input" && n.type === "checkbox" && n.checked && n.parent?.hasClass("acl-picker-option")); if (selector === '.acl-picker-option input[type="checkbox"]') return descendants.filter(n => n.tag === "input" && n.type === "checkbox" && n.parent?.hasClass("acl-picker-option")); const value = selector.match(/^\.acl-picker-option input\[value="([^"]+)"\]$/); if (value) return descendants.filter(n => n.tag === "input" && n.value === value[1] && n.parent?.hasClass("acl-picker-option")); const cls = selector.match(/^\.([\w-]+)$/); if (cls) return descendants.filter(n => n.hasClass(cls[1])); throw Error("Unsupported test selector " + selector); } querySelector(selector) { return this.querySelectorAll(selector)[0] || null; } } const profiles = Array.from({length:10}, (_,i) => ({ id: `profile_${i}`, name: i === 0 ? "Office networks" : `Profile ${i}`, acl: {default: "deny", rules:[{network:`10.${i}.0.0/16`,action:"allow"},{network:"192.0.2.0/24",action:"deny"}]}, country_access: {mode: i === 0 ? "block" : "off", countries: i === 0 ? ["RU"] : []} })); const context = { window: {}, envelope: {config:{acl_profiles:profiles}}, el(tag, text, cls) { const node = new DomNode(tag); if (text !== undefined) node.textContent = text; if (cls) node.className = cls; return node; }, t: key => key, Event: DomEvent, }; vm.createContext(context); vm.runInContext(fs.readFileSync(path.resolve(__dirname, "../src/admin/web/acl-picker.js"), "utf8"), context); const picker = context.window.rustProxyAclPicker; const root = picker.create(["profile_0", "profile_1"], "vh-acl-profile-choices"); assert.deepEqual(Array.from(picker.read(root)), ["profile_0", "profile_1"]); assert.equal(root.querySelectorAll(".acl-picker-tag").length, 2); assert.match(root.textContent, /10\.0\.0\.0\/16/); assert.match(root.textContent, /RU/); const search = root.querySelector(".acl-picker-search"); search.value = "Office"; search.dispatchEvent(new DomEvent("input", {bubbles:true})); assert.equal(root.querySelectorAll(".acl-picker-option").filter(node => !node.hidden).length, 1); search.value = ""; search.dispatchEvent(new DomEvent("input", {bubbles:true})); root.querySelector(".acl-picker-remove").click(); assert.deepEqual(Array.from(picker.read(root)), ["profile_1"]); for (const check of root.querySelectorAll('.acl-picker-option input[type="checkbox"]').slice(2,9)) { check.checked = true; check.dispatchEvent(new DomEvent("change", {bubbles:true})); } assert.equal(picker.count(root), 8); assert.equal(root.querySelector('.acl-picker-option input[value="profile_9"]').disabled, true); root.querySelector(".acl-picker-remove").click(); assert.equal(root.querySelector('.acl-picker-option input[value="profile_9"]').disabled, false); const docker = picker.create([], "docker-acl-profile"); assert.deepEqual(Array.from(picker.read(docker)), []); // empty override inherits agent profiles console.log("ACL picker DOM regression: PASS (choices, preview, search, badges, limit and inheritance)");