import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import vm from "node:vm"; import { test } from "node:test"; const read = name => readFileSync(new URL(name, import.meta.url), "utf8"); const util = vm.createContext({}); vm.runInContext(read("Util.js").replace(/^\.pragma library\s*/, ""), util); // Execute production QML JavaScript with synthetic services. This checks command // routing and reconciliation, not Qt bindings, delegate lifetime or real input. function panel(name, service, extra = {}) { const source = read(`${name}Panel.qml`); const serviceName = `${name.toLowerCase()}Service`; const context = vm.createContext({ [serviceName]: service, Util: util, ...extra }); context.root = context; const expression = source.slice(source.indexOf("readonly property var panelState:") + "readonly property var panelState:".length, source.indexOf("\n readonly property", source.indexOf("readonly property var panelState:") + 1)); Object.defineProperty(context, "panelState", { get: () => vm.runInContext(`(${expression})`, context) }); const start = source.indexOf(" function "); const end = source.indexOf("\n width:", start); vm.runInContext(source.slice(start, end), context); return context; } function serviceFixture(initial) { const fixture = { current: initial, calls: [], stateJson() { return JSON.stringify(this.current); } }; for (const name of ["setVolume", "toggleMute", "setDefaultSink", "setDefaultSource", "setAll", "setDisplay", "setupDdc", "action", "seek", "select", "sessionAction", "confirmAction", "cancelAction", "primaryActivate", "displayMenu", "secondaryActivate"]) fixture[name] = (...args) => { fixture.calls.push([name, ...args]); return '{"staleCommandReturn":true}'; }; return fixture; } function stableModel() { const rows = []; const operations = []; const context = vm.createContext({ get count() { return rows.length; }, get: index => rows[index], insert: (index, row) => { rows.splice(index, 0, { ...row }); operations.push("insert"); }, set: (index, row) => { Object.assign(rows[index], row); operations.push("set"); }, move: (from, to, count) => { rows.splice(to, 0, ...rows.splice(from, count)); operations.push("move"); }, remove: (index, count) => { rows.splice(index, count); operations.push("remove"); }, }); const source = read("StableListModel.qml"); vm.runInContext(source.slice(source.indexOf(" function sync("), source.lastIndexOf("\n}")), context); return { rows, operations, sync: context.sync }; } test("stable reconciliation updates and reorders surviving rows without removal", () => { const model = stableModel(); model.sync([{ id: "a", level: 0.2 }, { id: "b", level: 0.8 }]); const [a, b] = model.rows; model.operations.length = 0; model.sync([{ id: "a", level: 0.2 }, { id: "b", level: 0.8 }]); assert.deepEqual(model.operations, [], "unchanged snapshots do not emit model mutations"); model.sync([{ id: "b", level: 0.9 }, { id: "c", level: 0.5 }, { id: "a", level: 0.3 }]); assert.equal(model.rows[0], b); assert.equal(model.rows[2], a); assert.equal(a.rowData.level, 0.3); assert.equal(b.rowData.level, 0.9); assert(!model.operations.includes("remove")); model.sync([{ id: "a", level: 0.4 }]); assert.equal(model.rows[0], a); assert.equal(model.rows.length, 1); model.sync([]); assert.equal(model.rows.length, 0); }); test("every volume step reconciles into the same row during a simulated drag", () => { const model = stableModel(); const service = serviceFixture({ sinks: [{ id: "speaker", percent: 0 }] }); const audio = panel("Audio", service); model.sync(audio.panelState.sinks); const row = model.rows[0]; for (let step = 0; step <= 20; step++) { audio.setVolume("speaker", step * 0.075); service.current = { sinks: [{ id: "speaker", percent: Math.round(step * 7.5) }] }; model.sync(audio.panelState.sinks); assert.equal(model.rows[0], row); } assert.equal(service.calls.length, 21); assert.equal(row.rowData.percent, 150); assert.equal(model.operations.filter(op => op === "insert").length, 1); assert(!model.operations.includes("remove")); }); test("audio commands ignore returned snapshots and bound volume", () => { const service = serviceFixture({ marker: "initial" }); const audio = panel("Audio", service); audio.setVolume("sink", -1); audio.setVolume("sink", 2); audio.toggleMute("sink"); audio.selectSink("sink"); audio.selectSource("mic"); assert.deepEqual(service.calls, [["setVolume", "sink", 0], ["setVolume", "sink", 1.5], ["toggleMute", "sink"], ["setDefaultSink", "sink"], ["setDefaultSource", "mic"]]); service.current = { marker: "external" }; assert.equal(audio.panelState.marker, "external"); }); test("brightness routes bounded global and per-display changes without snapshot ownership", () => { const service = serviceFixture({ global: { mode: "uniform", level: 0.1 } }); const brightness = panel("Brightness", service, { hasGlobal: true }); brightness.setAllAt(2, 1); brightness.setDisplayAt("backlight", -1, 1); brightness.hasGlobal = false; brightness.setAllAt(1, 1); assert.deepEqual(service.calls, [["setAll", 1], ["setDisplay", "backlight", 0]]); service.current = { global: { mode: "uniform", level: 0.7 } }; assert.equal(brightness.panelState.global.level, 0.7); }); test("media capability matrix gates every transport and seek command", () => { const service = serviceFixture({ activePlayer: { title: "first" } }); const media = panel("Media", service, { nativePlayer: { canControl: true, canGoPrevious: false, canGoNext: false, canTogglePlaying: false }, player: { length: 200, canSeek: false, canShuffle: false, canRepeat: false, playbackState: "playing" }, }); const seekBinding = read("MediaPanel.qml").match(/readonly property bool canSeek: (.+)/)[1]; Object.defineProperty(media, "canSeek", { get: () => vm.runInContext(seekBinding, media) }); const capabilities = { shuffle: "canShuffle", repeat: "canRepeat", previous: "canGoPrevious", next: "canGoNext", playPause: "canTogglePlaying" }; for (const [action, flag] of Object.entries(capabilities)) { const owner = ["shuffle", "repeat"].includes(action) ? media.player : media.nativePlayer; assert.equal(media.actionEnabled(action), false); media.runAction(action); assert.equal(service.calls.length, 0); owner[flag] = true; assert.equal(media.actionEnabled(action), true); media.runAction(action); assert.deepEqual(service.calls.splice(0), [["action", action]]); media.nativePlayer.canControl = false; media.runAction(action); assert.equal(service.calls.length, 0); media.nativePlayer.canControl = true; owner[flag] = false; } media.seekByRatio(0.5); assert.equal(service.calls.length, 0); media.player.canSeek = true; media.nativePlayer.canControl = false; media.seekByRatio(0.5); media.nativePlayer.canControl = true; media.player.length = 0; media.seekByRatio(0.5); assert.equal(service.calls.length, 0, "seek requires control capability and a positive duration"); media.player.length = 200; media.seekByRatio(-1); media.seekByRatio(2); media.selectPlayer("other"); assert.deepEqual(service.calls, [["seek", 0], ["seek", 200], ["select", "other"]]); assert.equal(media.actionLabel("playPause"), "pause"); media.player.playbackState = "paused"; assert.equal(media.actionLabel("playPause"), "play"); service.current = { activePlayer: { title: "external" } }; assert.equal(media.panelState.activePlayer.title, "external"); media.nativePlayer = null; assert.equal(media.actionEnabled("playPause"), false); }); test("power dispatch does not take ownership of confirmation state", () => { const service = serviceFixture({ pendingConfirmation: "" }); const focused = []; const power = panel("Power", service, { safeActions: ["lock", "suspend", "monitor-off"], destructiveActions: ["logout", "reboot", "poweroff"], Qt: { TabFocusReason: "tab" }, actionRepeater: { itemAt: index => index < 0 ? null : { forceActiveFocus: reason => focused.push([index, reason]) } }, }); power.runSession("reboot"); service.current = { pendingConfirmation: "reboot" }; assert.equal(power.panelState.pendingConfirmation, "reboot"); power.confirm("reboot"); power.cancel(); service.current = { pendingConfirmation: "", battery: { percent: 42 } }; assert.equal(power.panelState.pendingConfirmation, ""); assert.equal(power.panelState.battery.percent, 42); assert.deepEqual(service.calls, [["sessionAction", "reboot"], ["confirmAction", "reboot"], ["cancelAction"]]); assert.deepEqual(focused, [[4, "tab"], [4, "tab"]]); }); test("tray primary, native menu and menu-only fallback route exactly once", () => { const service = serviceFixture({ items: [{ id: "app", title: "before" }] }); const tray = panel("Tray", service); const source = read("TrayPanel.qml"); const nativeMenu = { menu: null, anchor: {}, opens: 0, open() { this.opens++; } }; const cell = vm.createContext({ root: tray, rowData: { id: "app", onlyMenu: false }, width: 100, height: 60, nativeMenu, Qt: { rect: (x, y, width, height) => ({ x, y, width, height }) }, QsWindow: { window: "window" }, mapToItem: (_, x, y) => ({ x: x + 200, y: y + 100 }), }); cell.itemCell = cell; const start = source.indexOf(" function openMenu("); vm.runInContext(source.slice(start, source.indexOf("\n theme:", start)), cell); cell.activate(); assert.deepEqual(service.calls.splice(0), [["primaryActivate", "app", "window", 250, 130]]); cell.rowData.onlyMenu = true; cell.activate(); assert.deepEqual(service.calls.splice(0), [["displayMenu", "app", "window", 250, 130]]); nativeMenu.menu = {}; cell.activate(); assert.equal(nativeMenu.opens, 1); assert.deepEqual(nativeMenu.anchor.rect, { x: 50, y: 30, width: 1, height: 1 }); assert.equal(service.calls.length, 0); tray.secondary("app"); assert.deepEqual(service.calls, [["secondaryActivate", "app"]]); service.current = { items: [{ id: "app", title: "external" }] }; assert.equal(tray.panelState.items[0].title, "external"); }); test("panel source contracts retain state bindings and host-bounded scrolling", () => { for (const name of ["Audio", "Brightness", "Media", "Power", "Tray"]) { const source = read(`${name}Panel.qml`); assert.match(source, /readonly property var panelState:/); assert.doesNotMatch(source, /panelState\s*=|refreshFrom\(/); assert.match(source, /Flickable\s*\{\s*id: root/); assert.match(source, /contentHeight:/); assert.match(source, /clip: true/); } for (const name of ["Audio", "Brightness", "Tray"]) assert.match(read(`${name}Panel.qml`), /StableListModel\s*\{/); for (const name of ["Audio", "Brightness", "Media"]) assert.match(read(`${name}Panel.qml`), /Accessible\.name:.*(?:volume|brightness|playback)/); assert.doesNotMatch(read("AudioPanel.qml"), /MouseArea\s*\{/); assert.match(read("TrayPanel.qml"), /acceptedButtons: Qt\.MiddleButton \| Qt\.RightButton/); assert.doesNotMatch(read("PowerPanel.qml"), /Keys\.on(?:Return|Enter|Escape)Pressed/); });