import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import vm from "node:vm"; const read = name => readFileSync(new URL(name, import.meta.url), "utf8"); const protocol = vm.createContext({}); vm.runInContext(read("niri.js"), protocol); const qml = read("Niri.qml"); const plain = value => JSON.parse(JSON.stringify(value)); const equal = (got, want, message) => assert.deepEqual(plain(got), plain(want), message); const layout = (width = 800) => ({ pos_in_scrolling_layout: [1, 1], tile_size: [width, 600], window_size: [width, 600], tile_pos_in_workspace_view: [0, 0], window_offset_in_tile: [0, 0] }); const win = (id, focused = false) => ({ id, title: `window ${id}`, app_id: "synthetic.app", pid: 123, workspace_id: 1, is_focused: focused, is_floating: false, is_urgent: false, layout: layout(), focus_timestamp: null }); const ws = (id, output, active = false, focused = false) => ({ id, idx: id + 1, name: null, output, is_urgent: false, is_active: active, is_focused: focused, active_window_id: null }); const cast = (target = { Nothing: {} }, kind = "PipeWire") => ({ stream_id: 1, session_id: 1, kind, target, is_dynamic_target: false, is_active: false, pid: null, pw_node_id: null }); const output = (name, x) => ({ name, make: "fixture", model: "panel", serial: null, physical_size: [300, 200], modes: [{ width: 800, height: 600, refresh_rate: 60000, is_preferred: true }], current_mode: 0, is_custom_mode: false, vrr_supported: false, vrr_enabled: false, logical: { x, y: 0, width: 800, height: 600, scale: 1, transform: "Normal" } }); // Replay the actual QML function bodies, with only process/timer boundaries fake. // This is not a QML load, timer, input, or visual test. function service() { const process = () => ({ running: false, commands: [], signals: [], exec(command) { this.commands.push(plain(command)); this.running = true; }, signal(number) { this.signals.push(number); } }); const state = vm.createContext({ NiriState: protocol, available: true, ready: false, windowsInitialized: false, workspacesInitialized: false, streamFailed: false, retryDelay: 250, lastError: "", outputError: "", outputsPending: false, outputsFinished: false, windows: [], workspaces: [], outputs: {}, focusedWindow: null, overviewState: "unknown", lastWindowAction: {}, workspaceShortcuts: {}, actionQueue: [], query: process(), outputQuery: process(), actionProc: process(), reconnect: { running: false, interval: 0, starts: 0, stop() { this.running = false; }, start() { this.running = true; this.starts++; } }, captured: [], screenshotCaptured(path) { state.captured.push(path); } }); state.root = state; for (const match of qml.matchAll(/^ function (\w+)\(([^)]*)\) \{([\s\S]*?)^ \}/gm)) vm.runInContext(`function ${match[1]}(${match[2]}) {${match[3]}\n}`, state); Object.defineProperty(state, "focusedWindowRecord", { get: () => state.windowById(state.focusedWindow) }); return state; } const send = (state, name, data) => state.applyEvent(JSON.stringify({ [name]: data })); function initialize(state, windows = [win(0, true), win(2)], workspaces = [ws(1, "DP-1", true, true), ws(2, "DP-1"), ws(3, "DP-2", true)]) { send(state, "WorkspacesChanged", { workspaces }); send(state, "WindowsChanged", { windows }); send(state, "OverviewOpenedOrClosed", { is_open: false }); assert.equal(state.ready, true); } const state = service(); state.refresh(); equal(state.query.commands, [["niri", "msg", "--json", "event-stream"]]); state.refresh(); assert.equal(state.query.commands.length, 1, "never overlap subscriptions"); send(state, "FutureEvent", null); assert.equal(state.ready, false, "unknown events cannot establish readiness"); send(state, "ScreenshotCaptured", { path: null }); assert.equal(state.ready, false, "capture is not initial state"); send(state, "WorkspacesChanged", { workspaces: [ws(1, "DP-1", true, true), ws(2, "DP-1"), ws(3, "DP-2", true)] }); assert.equal(state.ready, false); send(state, "WindowsChanged", { windows: [win(0, true), win(2)] }); assert.equal(state.focusedWindow, 0, "initial focus allows id zero"); assert.equal(state.ready, false, "overview initial state is also required"); send(state, "OverviewOpenedOrClosed", { is_open: false }); assert.equal(state.ready, true); assert.equal(state.focusedWindowRecord.app_id, "synthetic.app"); send(state, "WorkspaceActivated", { id: 2, focused: false }); equal(state.workspaces.map(row => [row.is_active, row.is_focused]), [[false, true], [true, false], [true, false]]); send(state, "WorkspaceActivated", { id: 3, focused: true }); equal(state.workspaces.map(row => [row.is_active, row.is_focused]), [[false, false], [true, false], [true, true]]); assert.equal(state.focusedOutputNameFromWorkspaces(state.workspaces), "DP-2"); // Cross-part ordering is explicitly non-atomic upstream. send(state, "WorkspaceActiveWindowChanged", { workspace_id: 1, active_window_id: 99 }); assert.equal(state.workspaces[0].active_window_id, 99); assert.equal(state.windowById(99), null); assert(state.displayWorkspaces(state.workspaces).some(row => row.id === 1)); send(state, "WorkspaceActiveWindowChanged", { workspace_id: 1, active_window_id: null }); assert(!state.displayWorkspaces(state.workspaces).some(row => row.id === 1)); for (const urgent of [true, false]) { send(state, "WorkspaceUrgencyChanged", { id: 1, urgent }); assert.equal(state.workspaces[0].is_urgent, urgent); assert.equal(state.displayWorkspaces(state.workspaces).some(row => row.id === 1), urgent); } send(state, "WindowOpenedOrChanged", { window: win(99, true) }); equal(state.windows.filter(row => row.is_focused).map(row => row.id), [99]); assert.equal(state.focusedWindow, 99); const changed = { ...win(99, true), title: "renamed", app_id: "synthetic.vault-context", workspace_id: null }; send(state, "WindowOpenedOrChanged", { window: changed }); assert.equal(state.windows.length, 3); equal(state.focusedWindowRecord, changed, "whole record replacement, including title/app-id"); send(state, "WindowOpenedOrChanged", { window: { ...changed, is_focused: false, title: null, app_id: null } }); assert.equal(state.focusedWindow, null, "unfocused replacement of focused window clears derived focus"); send(state, "WindowFocusChanged", { id: 0 }); equal(state.windows.filter(row => row.is_focused).map(row => row.id), [0]); assert.equal(state.focusedWindow, 0); send(state, "WindowFocusChanged", { id: null }); assert.equal(state.focusedWindow, null); assert(state.windows.every(row => !row.is_focused)); send(state, "WindowFocusChanged", { id: 999 }); assert.equal(state.focusedWindow, null, "reference reducer only focuses records it has"); send(state, "WindowFocusChanged", { id: 99 }); for (const urgent of [true, false]) { send(state, "WindowUrgencyChanged", { id: 99, urgent }); assert.equal(state.windowById(99).is_urgent, urgent); } for (const focus_timestamp of [{ secs: 12, nanos: 34 }, null]) { send(state, "WindowFocusTimestampChanged", { id: 99, focus_timestamp }); equal(state.windowById(99).focus_timestamp, focus_timestamp); } send(state, "WindowLayoutsChanged", { changes: [[0, layout(900)], [99, layout(1200)]] }); equal(state.windowById(0).layout, layout(900)); equal(state.windowById(99).layout, layout(1200)); send(state, "WindowClosed", { id: 2 }); assert.equal(state.focusedWindow, 99, "closing a different window preserves focus"); send(state, "WindowClosed", { id: 99 }); assert.equal(state.focusedWindow, null); assert.equal(state.windowById(99), null); send(state, "WindowsChanged", { windows: [win(9, true)] }); equal(state.windows.map(row => row.id), [9], "full stream state replaces old records"); send(state, "WorkspacesChanged", { workspaces: [ws(0, null)] }); equal(state.workspaces.map(row => row.id), [0]); send(state, "WorkspaceActivated", { id: 0, focused: true }); assert.equal(state.workspaces[0].is_focused, true); send(state, "OverviewOpenedOrClosed", { is_open: true }); assert.equal(state.overviewState, "open"); for (const path of ["/tmp/synthetic.png", null, ""]) send(state, "ScreenshotCaptured", { path }); equal(state.captured, [null, "/tmp/synthetic.png", null, ""], "emit upstream paths verbatim, including null"); // Outputs cannot overwrite streamed focus, windows, workspaces, or overview, // even when a response arrives after newer deltas (the old snapshot race). const streamed = plain([state.windows, state.workspaces, state.focusedWindow, state.overviewState]); const inventory = { "DP-1": output("DP-1", 800), "DP-2": output("DP-2", -800) }; state.applyOutputs(JSON.stringify(inventory), 0, 0); equal([state.windows, state.workspaces, state.focusedWindow, state.overviewState], streamed); equal(state.outputs, inventory); equal(state.outputList().map(row => row.name), ["DP-1", "DP-2"]); send(state, "WorkspacesChanged", { workspaces: [ws(1, "DP-1", true), ws(2, "DP-2", true)] }); equal(state.workspaceGroups().map(row => row.output), ["DP-2", "DP-1"]); state.applyOutputs(JSON.stringify({ ...inventory, "DP-1": output("DP-1", -1600) }), 0, 0); equal(state.workspaceGroups().map(row => row.output), ["DP-1", "DP-2"], "geometry-only inventory refresh reorders groups"); const lastInventory = plain(state.outputs); for (const [text, code, status] of [ ["{}", 1, 0], ["{}", 0, 1], ["", 0, 0], ["{", 0, 0], ["null", 0, 0], ["[]", 0, 0], ['{"DP-1":{}}', 0, 0], [JSON.stringify({ "DP-1": { ...output("DP-1", 0), logical: {} } }), 0, 0] ]) { state.applyOutputs(text, code, status); assert(state.outputError.length > 0); equal(state.outputs, lastInventory, "failed inventory remains intact"); assert.equal(state.ready, true, "output failure does not invalidate stream readiness"); } state.applyOutputs(JSON.stringify({ "DP-1": { ...output("DP-1", 0), logical: null, current_mode: null } }), 0, 0); assert.equal(state.outputX("DP-1"), 1000000, "disabled output has no physical order"); assert.equal(state.outputError, ""); state.applyOutputs("{}", 0, 0); equal(state.outputList(), [], "successful empty inventory removes disconnected outputs"); state.refreshOutputs(); state.refreshOutputs(); state.refreshOutputs(); assert.equal(state.outputQuery.commands.length, 1); assert.equal(state.outputsPending, true); state.outputQuery.running = false; state.refreshOutputs(); assert.equal(state.outputsPending, false); equal(state.outputQuery.commands[1], ["niri", "msg", "--json", "outputs"]); // Validation is atomic, never an empty-success fallback or a swallowed error. const malformed = { WorkspacesChanged: [{}, { workspaces: null }, { workspaces: [ws(1, "DP-1"), ws(1, "DP-1")] }, { workspaces: [{ ...ws(1, "DP-1"), is_active: 1 }] }], WorkspaceActivated: [{ id: 1, focused: "true" }, { id: 999, focused: true }], WorkspaceActiveWindowChanged: [{ workspace_id: 1 }, { workspace_id: 999, active_window_id: null }], WorkspaceUrgencyChanged: [{ id: 1, urgent: 1 }], WindowsChanged: [{}, { windows: [win(0), win(0)] }, { windows: [{ ...win(0), layout: {} }] }], WindowOpenedOrChanged: [{}, { window: { ...win(0), title: 1 } }, { window: { ...win(0), is_focused: null } }], WindowClosed: [{ id: "0" }, { id: 999 }], WindowFocusChanged: [{}, { id: false }, { id: -1 }, { id: 0.5 }], WindowFocusTimestampChanged: [{ id: 0 }, { id: 0, focus_timestamp: { secs: 1, nanos: 1000000000 } }], WindowUrgencyChanged: [{ id: 0, urgent: "false" }], WindowLayoutsChanged: [{}, { changes: [[0, layout(1000)], [999, layout()]] }, { changes: [[0, {}]] }], OverviewOpenedOrClosed: [{}, { is_open: 1 }], ScreenshotCaptured: [{}, { path: 123 }, { path: false }], KeyboardLayoutsChanged: [{}, { keyboard_layouts: { names: [1], current_idx: 0 } }], KeyboardLayoutSwitched: [{}, { idx: 256 }], ConfigLoaded: [{}, { failed: "false" }], CastsChanged: [{}, { casts: [null] }], CastStartedOrChanged: [{}, { cast: { ...cast(), target: { Window: { id: "1" } } } }], CastStopped: [{}, { stream_id: -1 }] }; for (const [name, payloads] of Object.entries(malformed)) { for (const payload of [null, false, ...payloads]) { const s = service(); initialize(s); const before = plain([s.windows, s.workspaces, s.focusedWindow, s.overviewState]); send(s, name, payload); assert.equal(s.ready, false, `${name} rejects malformed payload`); assert.equal(s.streamFailed, true, name); assert.match(s.lastError, /malformed niri/); equal(s.query.signals, [9], "bad stream is terminated for resynchronization"); equal([s.windows, s.workspaces, s.focusedWindow, s.overviewState], before, "no partial mutation"); equal(s.captured, []); send(s, "WindowsChanged", { windows: [] }); equal(s.windows, before[0], "ignore remaining data from invalidated stream"); } } for (const line of ["{", "null", "[]", "{}", '{"WindowsChanged":{},"FutureEvent":{}}']) { const s = service(); s.applyEvent(line); assert.equal(s.streamFailed, true, line); assert.equal(s.ready, false); } const unowned = service(); for (const [name, payload] of [ ["KeyboardLayoutsChanged", { keyboard_layouts: { names: ["English (US)"], current_idx: 0 } }], ["KeyboardLayoutSwitched", { idx: 0 }], ["ConfigLoaded", { failed: false }], ["CastsChanged", { casts: [cast()] }], ["CastsChanged", { casts: [] }], ["CastStartedOrChanged", { cast: cast({ Output: { name: "DP-1" } }, "WlrScreencopy") }], ["CastStartedOrChanged", { cast: { ...cast({ Window: { id: 0 } }), pid: 123, pw_node_id: 4 } }], ["CastStopped", { stream_id: 1 }] ]) { send(unowned, name, payload); assert.equal(unowned.ready, false, `${name} is not owned state`); assert.equal(unowned.streamFailed, false, `${name} accepts its installed payload`); } const ignored = service(); initialize(ignored); const beforeIgnored = plain([ignored.windows, ignored.workspaces]); for (const name of ["FutureEvent", "OutputsChanged"]) send(ignored, name, { outputs: [] }); equal([ignored.windows, ignored.workspaces], beforeIgnored); assert.equal(ignored.streamFailed, false); send(ignored, "WindowFocusChanged", { id: 0, future_field: 42 }); assert.equal(ignored.focusedWindow, 0, "unknown fields remain tolerated"); for (const name of ["WorkspaceUrgencyChanged", "WindowUrgencyChanged"]) send(ignored, name, { id: 999, urgent: true }); send(ignored, "WindowFocusTimestampChanged", { id: 999, focus_timestamp: null }); assert.equal(ignored.streamFailed, false, "reference reducer ignores missing urgency/timestamp targets"); // Disconnect resets all streamed state, not the independently owned inventory. const reconnecting = service(); initialize(reconnecting); reconnecting.outputs = inventory; reconnecting.streamDisconnected(); assert.equal(reconnecting.ready, false); equal(reconnecting.windows, []); equal(reconnecting.workspaces, []); assert.equal(reconnecting.focusedWindow, null); assert.equal(reconnecting.overviewState, "unknown"); equal(reconnecting.outputs, inventory); assert.equal(reconnecting.reconnect.interval, 250); reconnecting.streamDisconnected(); assert.equal(reconnecting.reconnect.starts, 1, "duplicate disconnect notification is coalesced"); for (let attempt = 0; attempt < 20; attempt++) { reconnecting.reconnect.running = false; reconnecting.streamDisconnected(); assert(reconnecting.reconnect.interval >= 250 && reconnecting.reconnect.interval <= 8000); } assert.equal(reconnecting.reconnect.interval, 8000); reconnecting.refresh(); assert.equal(reconnecting.streamFailed, false); assert.equal(reconnecting.reconnect.running, false); send(reconnecting, "WindowsChanged", { windows: [] }); assert.equal(reconnecting.ready, false, "old initialization flags cannot satisfy a new stream"); send(reconnecting, "WorkspacesChanged", { workspaces: [] }); assert.equal(reconnecting.ready, false); send(reconnecting, "OverviewOpenedOrClosed", { is_open: false }); assert.equal(reconnecting.ready, true); assert.equal(reconnecting.retryDelay, 250); reconnecting.failStream("event stream initialization timed out"); assert.equal(reconnecting.ready, false); equal(reconnecting.query.signals, [9]); const unavailable = service(); unavailable.available = false; unavailable.refresh(); unavailable.refreshOutputs(); unavailable.streamDisconnected(); equal(unavailable.query.commands, []); equal(unavailable.outputQuery.commands, []); assert.equal(unavailable.reconnect.running, false); // Dormant IPC contract remains metadata only. Internal row id is not a CLI verb. const metadata = service(); initialize(metadata, [{ ...win(0, true), workspace_id: 0 }]); const card = JSON.parse(metadata.windowCardAction("toggle-fullscreen")); assert.equal(card.id, 0); assert.equal(card.workspace, 0); assert.equal(card.lastWindowAction.accepted, true); assert.equal(card.lastWindowAction.niriAction, "fullscreen-window"); assert.equal(card.lastWindowAction.executed, false); assert(card.actions.every(row => row.label === row.label.toLowerCase())); for (const kind of ["focus", "move-workspace", "close-window"]) assert.equal(JSON.parse(metadata.windowCardAction(kind)).lastWindowAction.accepted, true); assert.equal(JSON.parse(metadata.windowCardAction("unknown")).lastWindowAction.accepted, false); equal(metadata.actionProc.commands, [], "metadata never dispatches actions"); metadata.screenshot("/tmp/selection.png"); metadata.screenshotScreen("/tmp/screen.png"); metadata.screenshotWindow("/tmp/window.png"); assert.equal(metadata.actionProc.commands.length, 1, "rapid actions cannot terminate the preceding request"); assert.equal(metadata.actionQueue.length, 2); for (let i = 0; i < 2; i++) { metadata.actionProc.running = false; metadata.dispatchNextAction(); } assert.equal(metadata.actionQueue.length, 0); equal(metadata.actionProc.commands, [ ["niri", "msg", "action", "screenshot", "--path", "/tmp/selection.png"], ["niri", "msg", "action", "screenshot-screen", "--path", "/tmp/screen.png", "--write-to-disk", "true"], ["niri", "msg", "action", "screenshot-window", "--path", "/tmp/window.png", "--write-to-disk", "true"] ]); // Wiring checks supplement, not replace, the parent's nested integration gates. assert.doesNotMatch(qml, /loadSnapshot|applySnapshot|id: snapshot|"OutputsChanged"/); assert.match(qml, /signal screenshotCaptured\(var path\)/); assert.match(qml, /onRunningChanged: if \(!running\)\s*root\.streamDisconnected\(\)/, "handles clean exit, crash, and failed start"); assert.match(qml, /Connections \{\s*target: outputQuery\s*function onExited\(code, status\) \{\s*root.outputsFinished = true;\s*root.applyOutputs\(outputStdout.text, code, status\)/); assert.match(qml, /Connections \{\s*target: query\s*function onExited\(code\)/); assert.match(qml, /id: actionProc\s*onRunningChanged: if \(!running\)\s*root.dispatchNextAction\(\)/); assert.match(qml, /target: Quickshell[\s\S]*function onScreensChanged\(\)/); assert.match(qml, /interval: 5000\s*running: root.available\s*repeat: true\s*onTriggered: root.refreshOutputs\(\)/); assert.match(qml, /interval: 3000\s*running: outputQuery.running\s*onTriggered: outputQuery.signal\(9\)/); assert.match(qml, /running: query.running && !root.ready && !root.streamFailed/); assert.match(qml, /onTriggered: root.refresh\(\)/); console.log("niri.test.mjs: stream, lifecycle, outputs, screenshot signal, and metadata replays PASS (not UI validation)");