import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import vm from "node:vm"; const qml = readFileSync(new URL("./SurfaceManager.qml", import.meta.url), "utf8"); assert.match(qml, /import "popoutCatalog\.js" as PopoutCatalog/); const catalogSource = readFileSync(new URL("./popoutCatalog.js", import.meta.url), "utf8"); const catalog = vm.createContext({}); vm.runInContext(catalogSource.replace(/^\.pragma library\s*/, ""), catalog); const diagnostics = vm.createContext({ PopoutCatalog: catalog }); vm.runInContext(readFileSync(new URL("./diagnostics.js", import.meta.url), "utf8").replace(/^\.(?:pragma|import).*$/gm, ""), diagnostics); const start = qml.indexOf(" function parseRect("); const end = qml.indexOf(" Timer {", start); assert.ok(start >= 0 && end > start, "execute the actual SurfaceManager functions"); const surfaces = { screenWidth: 0, screenHeight: 0, barHeight: 34, vertical: false, goldenRatio: 1.61803, powerService: null, PopoutCatalog: catalog, DiagnosticData: diagnostics, operationId: 0, console: { info() {} }, popoutName: "clock", popoutAnchor: { x: 100, y: 0, width: 40, height: 34 }, popoutOpen: false, popoutClosing: false, popoutPhase: "closed", popoutOutput: "", popoutProgress: 0, activeLoaderName: "", pendingLoaderName: "", removingLoaderName: "", previousContent: {}, lastInteraction: "", lastCloseReason: "", dismissing() {}, niriService: null, returnWindowId: null, Qt: { rect: (x, y, width, height) => ({ x, y, width, height }) }, openTimer: { stop() {}, restart() {} }, unloadTimer: { stop() {} }, }; surfaces.root = surfaces; vm.createContext(surfaces); vm.runInContext(qml.slice(start, end), surfaces); const plain = value => JSON.parse(JSON.stringify(value)); // The public catalog is descriptive only: preserve its exact serialized shape and order. const expectedCatalog = { host: "PopoutHost.qml", loaderPolicy: "single-active-loader-with-close-retention", completeContent: true, entries: { clock: { component: "calendar-agenda", source: "Time.qml", visual: ["clock", "date", "calendar-grid", "agenda"] }, system: { component: "system-resources", source: "Resources.qml", visual: ["identity-card", "live-metrics", "sparklines", "issues-band", "top-processes"] }, notifications: { component: "notification-history", source: "Notifications.qml", visual: ["summary", "attention", "actions"] }, tray: { component: "tray-menu-host", source: "Tray.qml", visual: ["items", "attention", "native-menu-host"] }, media: { component: "media-controls", source: "Media.qml", visual: ["active-player", "transport", "progress", "selection"] }, noko: { component: "speech-transcription", source: "Noko.qml noko.mjs", visual: ["doctor", "setup", "record", "history"] }, clipboard: { component: "clipboard-history", source: "Clipboard.qml", visual: ["search", "cards", "thumbnails", "private-mode"] }, vault: { component: "vault-browser", source: "Vault.qml VaultPanel.qml vault-native", visual: ["search", "favorites", "details", "totp", "unlock"] }, launcher: { component: "app-launcher", source: "Launcher.qml", visual: ["app-search", "desktop-icons", "desktop-actions", "desktop-entry-details"] }, audio: { component: "audio-mixer", source: "Audio.qml", visual: ["sinks", "sources", "streams", "mute"] }, brightness: { component: "brightness-control", source: "Brightness.qml", visual: ["level", "luminance-shader", "osd"] }, connectivity: { component: "connectivity-panel", source: "Network.qml+Bluetooth.qml", visual: ["network", "wifi", "bluetooth", "devices"] }, power: { component: "power-battery", source: "Power.qml", visual: ["battery", "profile", "session", "confirmation"] }, }, }; assert.equal(surfaces.popoutContentCatalog(), JSON.stringify(expectedCatalog)); const heights = { clock: 580, system: 600, notifications: 460, tray: 440, media: 420, noko: 560, clipboard: 560, vault: 580, launcher: 432, audio: 520, brightness: 340, connectivity: 540, power: 188 }; assert.deepEqual(Object.keys(heights), Object.keys(expectedCatalog.entries)); const widths = { clock: 760, system: 780, notifications: 560, tray: 520, media: 560, noko: 640, clipboard: 520, vault: 840, launcher: 760, audio: 488, brightness: 488, connectivity: 620, power: 368 }; for (const [screenWidth, vertical] of [[0, false], [300, false], [1366, false], [2560, false], [1366, true], [34, true], [35, true]]) { Object.assign(surfaces, { screenWidth, vertical }); const available = screenWidth - (vertical ? 34 : 0); for (const [name, height] of Object.entries(heights)) { const width = available > 0 ? Math.min(widths[name], Math.max(1, available - 16)) : widths[name]; assert.equal(surfaces.popoutWidth(name), width); assert.deepEqual(plain(surfaces.popoutContentSize(name)), { width, height }, `${name}: width ${screenWidth}, vertical ${vertical}`); } } Object.assign(surfaces, { screenWidth: 1366, vertical: false }); for (const name of ["", "unknown", "screenshots", "toString", "constructor", "__proto__", null, undefined, 42]) { assert.deepEqual(plain(surfaces.popoutContentSize(name)), { width: 520, height: 300 }, `unknown name ${String(name)}`); } assert.deepEqual(plain(surfaces.popoutContentSize({ toString: () => "clock" })), { width: 760, height: 580 }); for (const [screenHeight, vertical, ceiling] of [[0, false, 680], [600, false, 518], [600, true, 552], [200, false, 260], [1080, false, 998]]) { Object.assign(surfaces, { screenHeight, vertical }); assert.equal(surfaces.heightBudget(2000), ceiling); assert.equal(surfaces.heightBudget(0), 220); assert.equal(surfaces.heightBudget("300"), Math.min(ceiling, 300)); for (const [name, height] of Object.entries(heights)) { assert.equal(surfaces.popoutContentSize(name).height, Math.min(ceiling, height), `${name}: screen height ${screenHeight}, vertical ${vertical}`); } } // Read fresh synthetic power state each time, including absent battery/devices. let powerState = {}; let powerReads = 0; surfaces.powerService = { stateJson() { powerReads++; return JSON.stringify(powerState); } }; Object.assign(surfaces, { screenWidth: 1366, screenHeight: 1080, vertical: false }); for (const [state, height] of [ [{}, 188], [{ battery: null, externalDevices: null }, 188], [{ battery: { present: false }, externalDevices: [{}] }, 232], [{ battery: { present: true } }, 248], [{ battery: { present: true }, externalDevices: [] }, 248], [{ battery: { present: true }, externalDevices: [{}] }, 292], [{ battery: { present: true }, externalDevices: [{}, {}] }, 292], [{ battery: { present: true }, externalDevices: [{}, {}, {}] }, 332], [{ pendingConfirmation: 'reboot' }, 304], [{ battery: { present: true }, pendingConfirmation: 'poweroff' }, 364], ]) { powerState = state; const before = powerReads; assert.deepEqual(plain(surfaces.popoutContentSize("power")), { width: 368, height }); assert.equal(powerReads, before + 1, "power sizing reads the live service once"); assert.deepEqual(plain(surfaces.powerContentSize()), { width: 368, height }); } surfaces.screenHeight = 200; assert.equal(surfaces.popoutContentSize("power").height, 260, "dynamic power height still respects the budget"); surfaces.powerService = null; assert.deepEqual(plain(surfaces.popoutContentSize("power")), { width: 368, height: 188 }); for (const name of ['media', 'tray']) { let rows = []; surfaces[`${name}Service`] = { stateJson: () => JSON.stringify({ [name === 'media' ? 'players' : 'items']: rows }) }; for (const vertical of [false, true]) { Object.assign(surfaces, { screenHeight: 768, vertical }); assert.deepEqual(plain(surfaces.popoutContentSize(name)), { width: 368, height: 104 }, `${name} empty popup shrinks in either orientation`); rows = [{}]; assert.deepEqual(plain(surfaces.popoutContentSize(name)), { width: widths[name], height: heights[name] }, `${name} arrival restores populated dimensions`); rows = []; assert.deepEqual(plain(surfaces.popoutContentSize(name)), { width: 368, height: 104 }, `${name} removal shrinks again`); } surfaces[`${name}Service`] = null; } // Consumers retain geometry and state computed by SurfaceManager. Object.assign(surfaces, { screenWidth: 1366, screenHeight: 768, vertical: false, popoutName: "clock" }); assert.deepEqual(plain(surfaces.popupWindowAnchorRect()), { x: 0, y: 34, width: 1366, height: 1 }); assert.deepEqual(plain(surfaces.popupCardFrame(1)), { source: "simple-center-slide-fade", progress: 1, currentRect: { x: 311, y: 42, width: 744, height: 564 }, opacity: 1, }); surfaces.vertical = true; assert.deepEqual(plain(surfaces.popupWindowAnchorRect()), { x: 34, y: 0, width: 1, height: 768 }); assert.deepEqual(plain(surfaces.popupCardFrame(1)), { source: "left-bar-slide-fade", progress: 1, currentRect: { x: 42, y: 102, width: 744, height: 564 }, opacity: 1, }); surfaces.vertical = false; const opened = JSON.parse(surfaces.openPopout("clock", "100,0,40,34", "synthetic")); assert.equal(opened.phase, "opening"); assert.equal(opened.activeLoaderName, "clock"); assert.deepEqual(opened.contentSize, { width: 760, height: 580 }); assert.equal(opened.transition.animation, "simple-center-slide-fade"); const switched = JSON.parse(surfaces.openPopout("power", "100,0,40,34", "synthetic")); assert.equal(switched.phase, "open"); assert.equal(switched.progress, 1); assert.deepEqual(switched.contentSize, { width: 368, height: 188 }); const closed = JSON.parse(surfaces.closePopout("escape-key")); assert.equal(closed.phase, "closed"); assert.equal(closed.activeLoaderName, ""); assert.equal(closed.lastCloseReason, "escape-key"); assert.deepEqual(closed.contentSize, { width: 520, height: 300 }); console.log("popout-catalog.test.mjs: ALL PASS");