import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import vm from 'node:vm'; const read = file => readFileSync(new URL(file, import.meta.url), 'utf8'); const catalog = vm.createContext({}); vm.runInContext(read('popoutCatalog.js').replace(/^\.pragma library\s*/, ''), catalog); const qml = read('SurfaceManager.qml'); const dismissed = []; let cancellations = 0; const s = vm.createContext({ popoutOpen: false, popoutClosing: false, popoutName: '', operationId: 0, niriService: { focusedWindow: 42 }, returnWindowId: null, powerService: { cancelAction() { cancellations++; }, stateJson: () => '{}' }, dismissing: name => dismissed.push(name), openTimer: { stop() {}, restart() {} }, unloadTimer: { stop() {} }, DiagnosticData: { nextOperationId: id => id + 1, transition: () => ({}) }, console: { info() {} }, Qt: { rect: (x, y, width, height) => ({ x, y, width, height }) }, }); s.root = s; vm.runInContext(qml.slice(qml.indexOf(' function parseRect('), qml.indexOf(' Timer {')), s); s.popoutState = () => s.popoutName; s.openPopout('clipboard', '0,0,34,34', ''); assert.equal(s.returnWindowId, 42); s.niriService.focusedWindow = null; s.hoverRegisteredPopout('launcher'); assert.equal(s.popoutName, 'clipboard'); assert.equal(dismissed.length, 0); s.openPopout('power', '0,0,34,34', ''); assert.equal(s.returnWindowId, 42); assert.deepEqual(dismissed, ['clipboard']); s.openPopout('audio', '0,0,34,34', ''); assert.equal(cancellations, 1); for (const reason of ['outside', 'escape-key', 'bar-toggle', 'popup-closed']) { s.openPopout('power', '0,0,34,34', ''); s.closePopout(reason); } assert.equal(cancellations, 5); s.closePopout('outside'); assert.equal(cancellations, 5, 'idempotent close does not cancel twice'); const service = read('Clipboard.qml'); assert.match(service, /Connections\s*\{\s*target: pasteFocusQuery\s*function onExited\(exitCode\)\s*\{\s*root\.checkPasteFocus\(exitCode\);/, 'focus query connects the Process signal, not an inert method'); const typed = []; let timerStarts = 0; let closed = 0; const c = vm.createContext({ pendingPasteId: '', pasteWindowId: null, pasteAttempts: 0, focusReply: '', lastAction: {}, surfaces: { popoutOpen: true, returnWindowId: 42, closePopout() { this.popoutOpen = false; closed++; } }, niriService: { focusedWindow: 42 }, rawRowById: id => id === 'item' ? { body: 'synthetic text' } : null, stateJson: () => '{}', Quickshell: { execDetached: args => typed.push([...args]) }, pasteFocusTimer: { start() { timerStarts++; }, restart() { timerStarts++; }, stop() {} }, }); vm.runInContext(service.slice(service.indexOf(' function paste('), service.indexOf(' function deleteRow(')), c); c.paste('missing'); assert.equal(c.lastAction.accepted, false); assert.equal(timerStarts, 0); c.paste('item'); assert.equal(closed, 1); assert.equal(c.lastAction.pending, true); assert.equal(typed.length, 0, 'never type before compositor focus proof'); c.paste('item'); assert.equal(closed, 1, 'one pending paste'); c.focusReply = 'null'; c.checkPasteFocus(0); assert.equal(typed.length, 0); c.focusReply = '{"id":42}'; c.checkPasteFocus(0); assert.deepEqual(typed, [['/usr/bin/wtype', '--', 'synthetic text']]); assert.equal(c.pendingPasteId, ''); assert.equal(c.lastAction.pending, false); c.checkPasteFocus(0); assert.equal(typed.length, 1, 'completion is idempotent'); c.paste('item'); c.focusReply = '{"id":99}'; c.checkPasteFocus(0); assert.equal(c.lastAction.error, 'paste destination lost focus'); assert.equal(typed.length, 1); c.paste('item'); c.surfaces.popoutOpen = true; c.checkPasteFocus(0); assert.equal(c.pendingPasteId, ''); assert.equal(typed.length, 1, 'new popup cancels pending typing'); c.surfaces.popoutOpen = false; c.paste('item'); for (let i = 0; i < 40; i++) { c.focusReply = ''; c.checkPasteFocus(1); } assert.equal(c.pendingPasteId, '', 'failed focus queries are bounded'); assert.equal(typed.length, 1); const util = vm.createContext({}); vm.runInContext(read('Util.js').replace(/^\.pragma library\s*/, ''), util); const viewport = { contentItem: {}, contentY: 100, contentHeight: 500, height: 120 }; const item = { height: 30, mapToItem: () => ({ y: 300 }) }; util.revealIn(item, viewport); assert.equal(viewport.contentY, 210); item.mapToItem = () => ({ y: 50 }); util.revealIn(item, viewport); assert.equal(viewport.contentY, 50); item.mapToItem = () => ({ y: 70 }); util.revealIn(item, viewport); assert.equal(viewport.contentY, 50, 'visible focus does not move the viewport'); console.log('popup-lifecycle.test.mjs: ALL PASS');