Files
NetMesh/electron/bridges/globalShortcutBridge.test.cjs

1773 lines
57 KiB
JavaScript
Raw Normal View History

const test = require("node:test");
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
function withPatchedTimers(run) {
const originalSetTimeout = global.setTimeout;
const originalClearTimeout = global.clearTimeout;
let nextTimerId = 1;
const timers = new Map();
global.setTimeout = (fn, _delay, ...args) => {
const id = nextTimerId++;
timers.set(id, () => fn(...args));
return id;
};
global.clearTimeout = (id) => {
timers.delete(id);
};
const flushNextTimer = () => {
const nextEntry = timers.entries().next().value;
if (!nextEntry) return false;
const [id, fn] = nextEntry;
timers.delete(id);
fn();
return true;
};
const getPendingTimerCount = () => timers.size;
return Promise.resolve()
.then(() => run({ flushNextTimer, getPendingTimerCount }))
.finally(() => {
global.setTimeout = originalSetTimeout;
global.clearTimeout = originalClearTimeout;
});
}
function withPatchedDateNow(initialValue, run) {
const originalDateNow = Date.now;
let currentValue = initialValue;
Date.now = () => currentValue;
return Promise.resolve()
.then(() =>
run({
setNow(nextValue) {
currentValue = nextValue;
},
}))
.finally(() => {
Date.now = originalDateNow;
});
}
function loadBridge() {
const bridgePath = require.resolve("./globalShortcutBridge.cjs");
delete require.cache[bridgePath];
return require("./globalShortcutBridge.cjs");
}
function createElectronStub() {
class FakeTray {
constructor() {
this.handlers = new Map();
this.contextMenu = null;
this.contextMenuPopped = false;
}
setToolTip() {}
setContextMenu(menu) {
this.contextMenu = menu;
}
popUpContextMenu() {
this.contextMenuPopped = true;
}
destroy() {}
on(eventName, handler) {
this.handlers.set(eventName, handler);
}
}
return {
Tray: FakeTray,
Menu: {
buildFromTemplate(template) {
return { template };
},
},
BrowserWindow: {
getAllWindows() {
return [];
},
},
globalShortcut: {
register() {
return true;
},
unregister() {},
},
nativeImage: {
createFromPath() {
return {
resize() {
return this;
},
setTemplateImage() {},
addRepresentation() {},
};
},
createEmpty() {
return {};
},
},
app: {
dock: {
menu: null,
setMenu(menu) {
this.menu = menu;
},
},
getAppPath() {
return process.cwd();
},
quit() {},
},
};
}
function createAppLockControllerStub(initialState = { locked: false, reason: null }) {
const listeners = new Set();
const state = {
locked: initialState.locked === true,
reason: initialState.reason ?? null,
};
return {
setLockedCalls: [],
state,
setLocked(reason) {
this.setLockedCalls.push(reason);
state.locked = true;
state.reason = reason;
for (const listener of listeners) {
try {
listener({ ...state });
} catch {
// ignore
}
}
return { ...state };
},
getRuntimeState() {
return { ...state };
},
unlock() {
state.locked = false;
state.reason = null;
for (const listener of listeners) {
try {
listener({ ...state });
} catch {
// ignore
}
}
return { ...state };
},
subscribe(listener) {
if (typeof listener !== "function") return () => {};
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
function createIpcMainStub() {
const handlers = new Map();
return {
handlers,
handle(channel, handler) {
handlers.set(channel, handler);
},
};
}
class FakeWindow extends EventEmitter {
constructor({ fullscreen = false } = {}) {
super();
this.fullscreen = fullscreen;
this.hideCalls = 0;
this.showCalls = 0;
this.focusCalls = 0;
this.restoreCalls = 0;
this.setFullScreenCalls = [];
this.destroyed = false;
this.minimized = false;
this.visible = true;
this.focused = true;
this.sentMessages = [];
this.webContents = {
send: (channel, ...args) => {
this.sentMessages.push([channel, ...args]);
},
};
}
isDestroyed() {
return this.destroyed;
}
isFullScreen() {
return this.fullscreen;
}
setFullScreen(nextValue) {
this.setFullScreenCalls.push(nextValue);
if (nextValue) {
this.fullscreen = true;
}
}
isMinimized() {
return this.minimized;
}
restore() {
this.restoreCalls += 1;
this.minimized = false;
}
isVisible() {
return this.visible;
}
isFocused() {
return this.focused;
}
hide() {
this.hideCalls += 1;
this.visible = false;
this.focused = false;
}
show() {
this.showCalls += 1;
this.visible = true;
this.emit("show");
}
focus() {
this.focusCalls += 1;
this.focused = true;
}
}
async function withPlatform(platform, run) {
const original = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", { configurable: true, value: platform });
try {
return await run();
} finally {
Object.defineProperty(process, "platform", original);
}
}
async function enableCloseToTray(bridge, electronModule = createElectronStub(), extraDeps = {}) {
bridge.init({
electronModule,
getMainWindow: () => electronModule.BrowserWindow.getAllWindows()[0] ?? null,
...extraDeps,
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: true });
return { ipcMain, electronModule };
}
test("handleWindowClose allows normal close when close-to-tray is disabled", () => {
const bridge = loadBridge();
const win = new FakeWindow();
let prevented = false;
const result = bridge.handleWindowClose({ preventDefault() { prevented = true; } }, win);
assert.equal(result, false);
assert.equal(prevented, false);
assert.equal(win.hideCalls, 0);
});
test("close-to-tray on a mac fullscreen window defers hide until after leave-full-screen and the trailing show", async () => {
// Observed macOS sequence after the red close on a fullscreen window:
// setFullScreen(false) → (animation) → leave-full-screen → trailing show
// Hiding before the trailing show causes macOS to pop the window back
// during the final space transition. The fix waits for the trailing show
// (or a fallback timer) before calling win.hide().
await withPatchedTimers(async ({ flushNextTimer, getPendingTimerCount }) => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
await enableCloseToTray(bridge);
const win = new FakeWindow({ fullscreen: true });
let prevented = false;
const result = bridge.handleWindowClose({ preventDefault() { prevented = true; } }, win);
assert.equal(result, true);
assert.equal(prevented, true);
assert.deepEqual(win.setFullScreenCalls, [false]);
assert.equal(win.hideCalls, 0);
// Watchdog timer is pending. No show listener yet — macOS's
// pre-leave-full-screen internal `show` events must not trigger hide.
assert.equal(getPendingTimerCount(), 1);
assert.equal(win.listenerCount("show"), 0);
// Spurious early show (mid-animation) does nothing.
win.emit("show");
assert.equal(win.hideCalls, 0);
assert.equal(getPendingTimerCount(), 1);
// leave-full-screen arrives. Watchdog cancelled; now we arm a `show`
// listener + trailing-show fallback timer. Still no hide.
win.fullscreen = false;
win.emit("leave-full-screen");
assert.equal(win.hideCalls, 0);
assert.equal(getPendingTimerCount(), 1);
assert.equal(win.listenerCount("show"), 1);
// Trailing show from macOS finalizing the space transition runs the hide.
win.emit("show");
assert.equal(win.hideCalls, 1);
assert.equal(win.listenerCount("show"), 0);
assert.equal(win.listenerCount("leave-full-screen"), 0);
assert.equal(win.listenerCount("closed"), 0);
assert.equal(getPendingTimerCount(), 0);
});
});
});
test("fallback timer hides the window when the trailing show never arrives", async () => {
await withPatchedTimers(async ({ flushNextTimer, getPendingTimerCount }) => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
await enableCloseToTray(bridge);
const win = new FakeWindow({ fullscreen: true });
bridge.handleWindowClose({ preventDefault() {} }, win);
win.fullscreen = false;
win.emit("leave-full-screen");
// Watchdog cleared; trailing-show fallback timer is pending.
assert.equal(getPendingTimerCount(), 1);
assert.equal(win.hideCalls, 0);
assert.equal(win.listenerCount("show"), 1);
// No show ever arrives. Fallback timer runs.
flushNextTimer();
assert.equal(win.hideCalls, 1);
assert.equal(win.listenerCount("show"), 0);
assert.equal(getPendingTimerCount(), 0);
});
});
});
test("watchdog forces the hide path if leave-full-screen never arrives", async () => {
await withPatchedTimers(async ({ flushNextTimer, getPendingTimerCount }) => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
await enableCloseToTray(bridge);
const win = new FakeWindow({ fullscreen: true });
bridge.handleWindowClose({ preventDefault() {} }, win);
assert.equal(getPendingTimerCount(), 1);
// Watchdog fires (simulates 5s with no leave-full-screen). It forces
// the leave path — which arms the trailing-show listener + fallback.
flushNextTimer();
assert.equal(win.hideCalls, 0);
assert.equal(getPendingTimerCount(), 1);
assert.equal(win.listenerCount("show"), 1);
// Trailing-show fallback fires → hide.
flushNextTimer();
assert.equal(win.hideCalls, 1);
assert.equal(getPendingTimerCount(), 0);
});
});
});
test("app activate clears a pending fullscreen hide", async () => {
// Regression for the close-to-tray + fullscreen bug where the internal
// `show` emitted during the fullscreen exit animation was cancelling the
// hide. main.cjs's app.on("activate") handler now calls into this bridge
// to cancel the pending hide when the user actually re-activates the app.
await withPatchedTimers(async ({ flushNextTimer, getPendingTimerCount }) => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
await enableCloseToTray(bridge);
const win = new FakeWindow({ fullscreen: true });
const result = bridge.handleWindowClose({ preventDefault() {} }, win);
assert.equal(result, true);
assert.equal(getPendingTimerCount(), 1);
bridge.clearPendingFullscreenHide(win);
assert.equal(getPendingTimerCount(), 0);
assert.equal(win.listenerCount("leave-full-screen"), 0);
assert.equal(win.listenerCount("closed"), 0);
assert.deepEqual(bridge.__testOnly?.getAppLockController?.().setLockedCalls ?? [], []);
assert.equal(flushNextTimer(), false);
assert.equal(win.hideCalls, 0);
});
});
});
test("focusing a visible window cancels a pending fullscreen hide", async () => {
await withPatchedTimers(async ({ getPendingTimerCount }) => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const win = new FakeWindow({ fullscreen: true });
win.focused = false;
electronModule.BrowserWindow.getAllWindows = () => [win];
let toggleWindow = null;
electronModule.globalShortcut.register = (_accelerator, handler) => {
toggleWindow = handler;
return true;
};
const { ipcMain } = await enableCloseToTray(bridge, electronModule, {
getMainWindow: () => win,
});
await ipcMain.handlers.get("netcatty:globalHotkey:register")(null, { hotkey: "Ctrl + `" });
const result = bridge.handleWindowClose({ preventDefault() {} }, win);
assert.equal(result, true);
assert.equal(getPendingTimerCount(), 1);
toggleWindow();
assert.equal(win.focusCalls, 1);
assert.equal(getPendingTimerCount(), 0);
assert.equal(win.listenerCount("leave-full-screen"), 0);
assert.equal(win.listenerCount("closed"), 0);
});
});
});
test("openMainWindow cancels a pending fullscreen hide before showing the window", async () => {
await withPatchedTimers(async ({ flushNextTimer, getPendingTimerCount }) => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const win = new FakeWindow({ fullscreen: true });
win.show = function showWithoutEmit() {
this.showCalls += 1;
this.visible = true;
};
electronModule.BrowserWindow.getAllWindows = () => [win];
const { ipcMain } = await enableCloseToTray(bridge, electronModule, {
getMainWindow: () => win,
});
const result = bridge.handleWindowClose({ preventDefault() {} }, win);
assert.equal(result, true);
assert.equal(getPendingTimerCount(), 1);
await ipcMain.handlers.get("netcatty:trayPanel:openMainWindow")();
assert.equal(win.showCalls, 1);
assert.equal(getPendingTimerCount(), 0);
const flushed = flushNextTimer();
assert.equal(flushed, false);
assert.equal(win.hideCalls, 0);
});
});
});
test("openMainWindow notifies renderer to lock on reopen", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const win = new FakeWindow();
electronModule.BrowserWindow.getAllWindows = () => [win];
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
await ipcMain.handlers.get("netcatty:trayPanel:openMainWindow")();
assert.deepEqual(win.sentMessages, [["netcatty:app-lock:reopen"]]);
});
test("tray session menu reveal cancels a pending fullscreen hide before focusing a session", async () => {
await withPatchedTimers(async ({ flushNextTimer, getPendingTimerCount }) => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const appLockController = createAppLockControllerStub();
const win = new FakeWindow({ fullscreen: true });
win.show = function showWithoutEmit() {
this.showCalls += 1;
this.visible = true;
};
electronModule.BrowserWindow.getAllWindows = () => [win];
bridge.init({
electronModule,
getMainWindow: () => win,
getAppLockController: () => appLockController,
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await withPlatform("darwin", async () => {
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: true });
const result = bridge.handleWindowClose({ preventDefault() {} }, win);
assert.equal(result, true);
assert.equal(getPendingTimerCount(), 1);
});
await withPlatform("linux", async () => {
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
sessions: [{ id: "s1", label: "dev", hostLabel: "dev.example", status: "connected" }],
});
const sessionItem = bridge.getTray().contextMenu.template
.filter((item) => typeof item.click === "function")
.find((item) => String(item.label).includes("dev.example"));
sessionItem.click();
await Promise.resolve();
assert.equal(win.showCalls, 1);
assert.equal(getPendingTimerCount(), 0);
assert.equal(win.listenerCount("leave-full-screen"), 0);
assert.equal(win.listenerCount("closed"), 0);
assert.equal(flushNextTimer(), false);
assert.equal(win.hideCalls, 0);
assert.deepEqual(appLockController.setLockedCalls, []);
assert.deepEqual(win.sentMessages.slice(0, 2), [
["netcatty:app-lock:reopen"],
["netcatty:tray:focusSession", "s1"],
]);
});
});
});
test("tray port-forward menu reveal cancels a pending fullscreen hide before toggling", async () => {
await withPatchedTimers(async ({ flushNextTimer, getPendingTimerCount }) => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const appLockController = createAppLockControllerStub();
const win = new FakeWindow({ fullscreen: true });
win.show = function showWithoutEmit() {
this.showCalls += 1;
this.visible = true;
};
electronModule.BrowserWindow.getAllWindows = () => [win];
bridge.init({
electronModule,
getAppLockController: () => appLockController,
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await withPlatform("darwin", async () => {
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: true });
const result = bridge.handleWindowClose({ preventDefault() {} }, win);
assert.equal(result, true);
assert.equal(getPendingTimerCount(), 1);
});
await withPlatform("linux", async () => {
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
portForwardRules: [{
id: "pf1",
label: "ssh",
type: "local",
localPort: 8080,
remoteHost: "host",
remotePort: 80,
status: "active",
}],
});
const portForwardItem = bridge.getTray().contextMenu.template
.filter((item) => typeof item.click === "function")
.find((item) => String(item.label).includes("ssh"));
portForwardItem.click();
assert.equal(win.showCalls, 1);
assert.equal(getPendingTimerCount(), 0);
assert.equal(win.listenerCount("leave-full-screen"), 0);
assert.equal(win.listenerCount("closed"), 0);
assert.equal(flushNextTimer(), false);
assert.equal(win.hideCalls, 0);
assert.deepEqual(appLockController.setLockedCalls, []);
assert.deepEqual(win.sentMessages.slice(0, 2), [
["netcatty:app-lock:reopen"],
["netcatty:tray:togglePortForward", "pf1", false],
]);
});
});
});
test("closing the window clears a pending fullscreen hide", async () => {
await withPatchedTimers(async ({ flushNextTimer, getPendingTimerCount }) => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
await enableCloseToTray(bridge);
const win = new FakeWindow({ fullscreen: true });
const result = bridge.handleWindowClose({ preventDefault() {} }, win);
assert.equal(result, true);
assert.equal(getPendingTimerCount(), 1);
assert.equal(win.listenerCount("leave-full-screen"), 1);
assert.equal(win.listenerCount("closed"), 1);
win.destroyed = true;
win.emit("closed");
assert.equal(getPendingTimerCount(), 0);
assert.equal(win.listenerCount("leave-full-screen"), 0);
assert.equal(win.listenerCount("closed"), 0);
assert.equal(flushNextTimer(), false);
assert.equal(win.hideCalls, 0);
});
});
});
test("disabling close-to-tray clears a pending fullscreen hide", async () => {
await withPatchedTimers(async ({ flushNextTimer, getPendingTimerCount }) => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const win = new FakeWindow({ fullscreen: true });
electronModule.BrowserWindow.getAllWindows = () => [win];
const { ipcMain } = await enableCloseToTray(bridge, electronModule, {
getMainWindow: () => win,
});
const result = bridge.handleWindowClose({ preventDefault() {} }, win);
assert.equal(result, true);
assert.equal(getPendingTimerCount(), 1);
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: false });
assert.equal(getPendingTimerCount(), 0);
assert.equal(win.listenerCount("leave-full-screen"), 0);
assert.equal(win.listenerCount("closed"), 0);
assert.equal(flushNextTimer(), false);
assert.equal(win.hideCalls, 0);
});
});
});
test("handleWindowClose hides immediately when tray close is used outside fullscreen", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
await enableCloseToTray(bridge);
const win = new FakeWindow({ fullscreen: false });
let prevented = false;
const result = bridge.handleWindowClose({ preventDefault() { prevented = true; } }, win);
assert.equal(result, true);
assert.equal(prevented, true);
assert.deepEqual(win.setFullScreenCalls, []);
assert.equal(win.hideCalls, 1);
});
});
test("handleWindowClose locks app runtime before hiding to tray", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const appLockController = createAppLockControllerStub();
bridge.init({
electronModule: createElectronStub(),
getAppLockController: () => appLockController,
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: true });
const win = new FakeWindow({ fullscreen: false });
bridge.handleWindowClose({ preventDefault() {} }, win);
assert.deepEqual(appLockController.setLockedCalls, ["background"]);
assert.equal(win.hideCalls, 1);
});
});
test("handleWindowClose stays in close-to-tray mode even if hide fails", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const appLockController = createAppLockControllerStub();
bridge.init({
electronModule: createElectronStub(),
getAppLockController: () => appLockController,
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: true });
const win = new FakeWindow({ fullscreen: false });
win.hide = function failingHide() {
throw new Error("hide failed");
};
let prevented = false;
const result = bridge.handleWindowClose({ preventDefault() { prevented = true; } }, win);
assert.equal(result, true);
assert.equal(prevented, true);
assert.equal(win.visible, true);
assert.deepEqual(appLockController.setLockedCalls, []);
});
});
test("tray icon event registration is platform-dependent", async () => {
// Test win32 platform
await withPlatform("win32", async () => {
const bridge = loadBridge();
await enableCloseToTray(bridge);
const trayInstance = bridge.getTray();
assert.ok(trayInstance, "Tray instance should be created");
assert.ok(trayInstance.handlers.has("click"), "win32 tray should have click handler");
assert.ok(trayInstance.handlers.has("right-click"), "win32 tray should have right-click handler");
assert.equal(trayInstance.contextMenu, null, "win32 tray should not set a context menu");
bridge.cleanup();
});
// Test Linux platform
await withPlatform("linux", async () => {
const bridge = loadBridge();
const { ipcMain } = await enableCloseToTray(bridge);
const trayInstance = bridge.getTray();
assert.ok(trayInstance, "Tray instance should be created");
assert.ok(trayInstance.handlers.has("click"), "linux tray should have click handler");
assert.ok(!trayInstance.handlers.has("right-click"), "linux tray should not use right-click handler");
assert.ok(trayInstance.contextMenu, "linux tray should have a native context menu");
const labels = trayInstance.contextMenu.template.map((item) => item.label);
assert.ok(labels.includes("Open Main Window"), "linux context menu should include Open Main Window");
assert.ok(labels.includes("Quit"), "linux context menu should include Quit");
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
sessions: [{ id: "s1", label: "dev", hostLabel: "dev.example", status: "connected" }],
});
const updatedLabels = trayInstance.contextMenu.template
.map((item) => item.label)
.filter(Boolean);
assert.ok(
updatedLabels.some((label) => label.includes("dev.example")),
"linux context menu should rebuild when tray menu data changes",
);
const win = new FakeWindow();
win.minimized = true;
const initializedElectronModule = {
...createElectronStub(),
BrowserWindow: {
getAllWindows() {
return [win];
},
},
};
bridge.init({
electronModule: initializedElectronModule,
getMainWindow: () => win,
getAppLockController: () => null,
});
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
sessions: [{ id: "s1", label: "dev", hostLabel: "dev.example", status: "connected" }],
portForwardRules: [{ id: "pf1", label: "ssh", type: "local", localPort: 8080, remoteHost: "host", remotePort: 80, status: "active" }],
});
const clickableItems = bridge.getTray().contextMenu.template.filter((item) => typeof item.click === "function");
const sessionItem = clickableItems.find((item) => String(item.label).includes("dev.example"));
const portForwardItem = clickableItems.find((item) => String(item.label).includes("ssh"));
sessionItem.click();
await Promise.resolve();
assert.deepEqual(win.sentMessages.slice(0, 2), [
["netcatty:app-lock:reopen"],
["netcatty:tray:focusSession", "s1"],
]);
win.sentMessages = [];
portForwardItem.click();
assert.deepEqual(win.sentMessages.slice(0, 2), [
["netcatty:app-lock:reopen"],
["netcatty:tray:togglePortForward", "pf1", false],
]);
bridge.cleanup();
});
// Locked runtime defers the toggle until unlock, while still reopening the window.
await withPlatform("linux", async () => {
const bridge = loadBridge();
const appLockController = createAppLockControllerStub();
const win = new FakeWindow();
win.minimized = true;
const initializedElectronModule = {
...createElectronStub(),
BrowserWindow: {
getAllWindows() {
return [win];
},
},
};
bridge.init({
electronModule: initializedElectronModule,
getMainWindow: () => win,
getAppLockController: () => appLockController,
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: true });
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
portForwardRules: [{
id: "pf1",
label: "ssh",
type: "local",
localPort: 8080,
remoteHost: "host",
remotePort: 80,
status: "active",
}],
});
const portForwardItem = bridge.getTray().contextMenu.template
.filter((item) => typeof item.click === "function")
.find((item) => String(item.label).includes("ssh"));
appLockController.setLocked("background");
portForwardItem.click();
assert.deepEqual(win.sentMessages, [
["netcatty:app-lock:reopen"],
], "toggle must not fire while runtime is locked");
assert.equal(bridge.__getPendingPortForwardTogglesForTests().length, 1);
appLockController.unlock();
assert.deepEqual(win.sentMessages, [
["netcatty:app-lock:reopen"],
["netcatty:tray:togglePortForward", "pf1", false],
], "queued toggle flushes after unlock");
assert.equal(bridge.__getPendingPortForwardTogglesForTests().length, 0);
bridge.cleanup();
});
// Test other platform (darwin)
await withPlatform("darwin", async () => {
const bridge = loadBridge();
await enableCloseToTray(bridge);
const trayInstance = bridge.getTray();
assert.ok(trayInstance, "Tray instance should be created");
assert.ok(trayInstance.handlers.has("click"), "darwin tray should have click handler");
assert.ok(!trayInstance.handlers.has("right-click"), "darwin tray should not have right-click handler");
assert.equal(trayInstance.contextMenu, null, "darwin tray should not set a context menu");
bridge.cleanup();
});
});
test("native tray sends an explicit stop for a runtime-present error rule", async () => {
await withPlatform("linux", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const sentMessages = [];
const win = new FakeWindow();
win.webContents = {
send(channel, ...args) {
sentMessages.push([channel, ...args]);
},
};
electronModule.BrowserWindow.getAllWindows = () => [win];
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
portForwardRules: [{
id: "cleanup-failed-rule",
label: "Cleanup failed",
type: "local",
localPort: 8080,
remoteHost: "127.0.0.1",
remotePort: 80,
status: "error",
canStop: true,
}],
});
const ruleItem = bridge.getTray().contextMenu.template.find(
(item) => item.label?.includes("Cleanup failed"),
);
assert.ok(ruleItem);
ruleItem.click();
assert.deepEqual(sentMessages, [
["netcatty:app-lock:reopen"],
["netcatty:tray:togglePortForward", "cleanup-failed-rule", false],
]);
bridge.cleanup();
});
});
test("mac dock menu lists saved hosts and forwards connect actions", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const sentMessages = [];
const win = new FakeWindow();
win.webContents = {
send(channel, ...args) {
sentMessages.push([channel, ...args]);
},
};
electronModule.BrowserWindow.getAllWindows = () => [win];
bridge.init({
electronModule,
getMainWindow: () => win,
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
hosts: [
{ id: "plain", label: "Plain Host", hostname: "plain.example" },
{ id: "pinned", label: "Pinned Host", hostname: "pinned.example", pinned: true },
{ id: "recent", label: "Recent Host", hostname: "recent.example", lastConnectedAt: 20 },
],
});
const dockTemplate = electronModule.app.dock.menu?.template ?? [];
const connectionMenu = dockTemplate.find((item) => item.label === "New Connection");
assert.ok(connectionMenu, "dock menu should expose a new connection submenu");
assert.deepEqual(
connectionMenu.submenu.map((item) => item.label),
["Pinned Host", "Recent Host", "Plain Host"],
);
await connectionMenu.submenu[0].click();
assert.deepEqual(sentMessages, [
["netcatty:app-lock:reopen"],
["netcatty:trayPanel:connectToHost", "pinned"],
]);
});
});
test("mac dock host connections wait until App Lock is unlocked", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const appLockController = createAppLockControllerStub();
const sentMessages = [];
const win = new FakeWindow();
win.webContents = {
send(channel, ...args) {
sentMessages.push([channel, ...args]);
},
};
electronModule.BrowserWindow.getAllWindows = () => [win];
bridge.init({
electronModule,
getMainWindow: () => win,
getAppLockController: () => appLockController,
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
hosts: [{ id: "target", label: "Target Host", hostname: "target.example" }],
});
const unlockedTemplate = electronModule.app.dock.menu.template;
const staleHostItem = unlockedTemplate
.find((item) => item.label === "New Connection")
.submenu[0];
appLockController.setLocked("manual");
const lockedTemplate = electronModule.app.dock.menu.template;
const lockedConnectionMenu = lockedTemplate.find((item) => item.label === "New Connection");
assert.equal(lockedConnectionMenu.enabled, false);
assert.deepEqual(lockedConnectionMenu.submenu.map((item) => item.label), ["No Saved Hosts"]);
await staleHostItem.click();
assert.deepEqual(sentMessages, [["netcatty:app-lock:reopen"]]);
appLockController.unlock();
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(sentMessages, [
["netcatty:app-lock:reopen"],
["netcatty:trayPanel:connectToHost", "target"],
]);
bridge.cleanup();
});
});
test("mac dock host click creates a main window when none exists", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const sentMessages = [];
const createdWin = new FakeWindow();
createdWin.webContents = {
send(channel, ...args) {
sentMessages.push([channel, ...args]);
},
};
electronModule.BrowserWindow.getAllWindows = () => [];
let createCalls = 0;
bridge.init({
electronModule,
ensureMainWindow: async () => {
createCalls += 1;
return createdWin;
},
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
hosts: [
{ id: "target", label: "Target Host", hostname: "target.example" },
],
});
const dockTemplate = electronModule.app.dock.menu?.template ?? [];
const connectionMenu = dockTemplate.find((item) => item.label === "New Connection");
await connectionMenu.submenu[0].click();
assert.equal(createCalls, 1);
assert.deepEqual(sentMessages, [
["netcatty:app-lock:reopen"],
["netcatty:trayPanel:connectToHost", "target"],
]);
});
});
test("mac dock host click waits for a newly created main window to be ready", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const sentMessages = [];
const createdWin = new FakeWindow();
createdWin.webContents = {
send(channel, ...args) {
sentMessages.push([channel, ...args]);
},
};
electronModule.BrowserWindow.getAllWindows = () => [];
let releaseReady;
bridge.init({
electronModule,
ensureMainWindow: async () => createdWin,
sendWhenRendererReady: async (win, channel, payload) => {
assert.equal(win, createdWin);
await new Promise((resolve) => {
releaseReady = resolve;
});
win.webContents.send(channel, payload);
return { success: true };
},
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
hosts: [
{ id: "target", label: "Target Host", hostname: "target.example" },
],
});
const dockTemplate = electronModule.app.dock.menu?.template ?? [];
const connectionMenu = dockTemplate.find((item) => item.label === "New Connection");
const clickPromise = connectionMenu.submenu[0].click();
for (let i = 0; i < 5 && !releaseReady; i += 1) {
await Promise.resolve();
}
assert.deepEqual(sentMessages, [["netcatty:app-lock:reopen"]]);
releaseReady();
await clickPromise;
assert.deepEqual(sentMessages, [
["netcatty:app-lock:reopen"],
["netcatty:trayPanel:connectToHost", "target"],
]);
});
});
test("mac dock host click waits for a tracked main window to be ready", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const sentMessages = [];
const win = new FakeWindow();
win.webContents = {
send(channel, ...args) {
sentMessages.push([channel, ...args]);
},
};
electronModule.BrowserWindow.getAllWindows = () => [win];
let releaseReady;
let createCalls = 0;
bridge.init({
electronModule,
getMainWindow: () => win,
ensureMainWindow: async () => {
createCalls += 1;
return win;
},
sendWhenRendererReady: async (target, channel, payload) => {
assert.equal(target, win);
await new Promise((resolve) => {
releaseReady = resolve;
});
target.webContents.send(channel, payload);
return { success: true };
},
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, {
hosts: [
{ id: "target", label: "Target Host", hostname: "target.example" },
],
});
const dockTemplate = electronModule.app.dock.menu?.template ?? [];
const connectionMenu = dockTemplate.find((item) => item.label === "New Connection");
const clickPromise = connectionMenu.submenu[0].click();
for (let i = 0; i < 5 && !releaseReady; i += 1) {
await Promise.resolve();
}
assert.equal(createCalls, 0);
assert.deepEqual(sentMessages, [["netcatty:app-lock:reopen"]]);
releaseReady();
await clickPromise;
assert.deepEqual(sentMessages, [
["netcatty:app-lock:reopen"],
["netcatty:trayPanel:connectToHost", "target"],
]);
});
});
test("mac dock open main window creates a main window when none exists", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const createdWin = new FakeWindow();
electronModule.BrowserWindow.getAllWindows = () => [];
let createCalls = 0;
bridge.init({
electronModule,
ensureMainWindow: async () => {
createCalls += 1;
return createdWin;
},
});
const dockTemplate = electronModule.app.dock.menu?.template ?? [];
const openMainItem = dockTemplate.find((item) => item.label === "Open Main Window");
await openMainItem.click();
assert.equal(createCalls, 1);
assert.equal(createdWin.showCalls, 1);
});
});
test("tray panel open main window creates a main window when none exists", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const createdWin = new FakeWindow();
electronModule.BrowserWindow.getAllWindows = () => [];
let createCalls = 0;
bridge.init({
electronModule,
ensureMainWindow: async () => {
createCalls += 1;
return createdWin;
},
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
await ipcMain.handlers.get("netcatty:trayPanel:openMainWindow")();
assert.equal(createCalls, 1);
assert.equal(createdWin.showCalls, 1);
});
});
test("tray panel session jump waits for a newly created main window to be ready", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const sentMessages = [];
const createdWin = new FakeWindow();
createdWin.webContents = {
send(channel, ...args) {
sentMessages.push([channel, ...args]);
},
};
electronModule.BrowserWindow.getAllWindows = () => [];
let releaseReady;
bridge.init({
electronModule,
ensureMainWindow: async () => createdWin,
sendWhenRendererReady: async (win, channel, payload) => {
assert.equal(win, createdWin);
await new Promise((resolve) => {
releaseReady = resolve;
});
win.webContents.send(channel, payload);
return { success: true };
},
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
const jumpPromise = ipcMain.handlers.get("netcatty:trayPanel:jumpToSession")(null, "session-1");
for (let i = 0; i < 5 && !releaseReady; i += 1) {
await Promise.resolve();
}
assert.deepEqual(sentMessages, []);
releaseReady();
await jumpPromise;
assert.deepEqual(sentMessages, [["netcatty:trayPanel:jumpToSession", "session-1"]]);
});
});
test("tray panel session close forwards without focusing the main window", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const sentMessages = [];
const mainWin = new FakeWindow();
mainWin.visible = false;
mainWin.focused = false;
mainWin.webContents = {
send(channel, ...args) {
sentMessages.push([channel, ...args]);
},
};
electronModule.BrowserWindow.getAllWindows = () => [mainWin];
let createCalls = 0;
bridge.init({
electronModule,
getMainWindow: () => mainWin,
ensureMainWindow: async () => {
createCalls += 1;
return mainWin;
},
sendWhenRendererReady: async (win, channel, payload) => {
assert.equal(win, mainWin);
win.webContents.send(channel, payload);
return { success: true };
},
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
const result = await ipcMain.handlers.get("netcatty:trayPanel:closeSession")(null, "session-1");
assert.deepEqual(result, { success: true });
assert.deepEqual(sentMessages, [["netcatty:trayPanel:closeSession", "session-1"]]);
assert.equal(createCalls, 0);
assert.equal(mainWin.showCalls, 0);
assert.equal(mainWin.focusCalls, 0);
assert.equal(mainWin.isVisible(), false);
});
});
test("tray panel session close does not create a missing main window", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
electronModule.BrowserWindow.getAllWindows = () => [];
let createCalls = 0;
bridge.init({
electronModule,
getMainWindow: () => null,
ensureMainWindow: async () => {
createCalls += 1;
return new FakeWindow();
},
});
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
const result = await ipcMain.handlers.get("netcatty:trayPanel:closeSession")(null, "stale-session");
assert.deepEqual(result, { success: false, error: "Main window is not available" });
assert.equal(createCalls, 0);
});
});
test("tray menu updates refresh an already open tray panel", async () => {
await withPlatform("darwin", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const sentMessages = [];
class FakePanelWindow extends FakeWindow {
constructor() {
super();
const webContents = new EventEmitter();
webContents.send = (channel, ...args) => {
sentMessages.push([channel, ...args]);
};
this.webContents = webContents;
}
async loadURL() {}
getBounds() { return { width: 360, height: 520 }; }
setBounds() {}
destroy() { this.destroyed = true; }
}
FakePanelWindow.getAllWindows = () => [];
electronModule.BrowserWindow = FakePanelWindow;
electronModule.screen = {
getDisplayNearestPoint: () => ({ workArea: { x: 0, y: 0, width: 1440, height: 900 } }),
};
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
const trayInstance = bridge.getTray();
trayInstance.getBounds = () => ({ x: 100, y: 0, width: 24, height: 24 });
trayInstance.handlers.get("click")();
sentMessages.length = 0;
const sessions = [{ id: "remaining", label: "Remaining", status: "connected" }];
await ipcMain.handlers.get("netcatty:tray:updateMenuData")(null, { sessions });
assert.deepEqual(sentMessages, [["netcatty:trayPanel:setMenuData", {
sessions,
portForwardRules: [],
hosts: [],
}]]);
bridge.cleanup();
});
});
test("toggleWindowVisibility show path delegates to showAndFocusMainWindow on win32", async () => {
await withPlatform("win32", async () => {
const windowManagerPath = require.resolve("./windowManager.cjs");
const actualWindowManager = require(windowManagerPath);
const showCalls = [];
let appFocusCalls = 0;
require.cache[windowManagerPath].exports = {
...actualWindowManager,
showAndFocusMainWindow(win) {
showCalls.push(win);
return true;
},
};
try {
const bridge = loadBridge();
const electronModule = createElectronStub();
electronModule.app.focus = () => {
appFocusCalls += 1;
};
const win = new FakeWindow();
win.visible = false;
win.focused = false;
electronModule.BrowserWindow.getAllWindows = () => [win];
let toggleWindow = null;
electronModule.globalShortcut.register = (_accelerator, handler) => {
toggleWindow = handler;
return true;
};
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
await ipcMain.handlers.get("netcatty:globalHotkey:register")(null, { hotkey: "Ctrl + `" });
assert.ok(toggleWindow, "expected global hotkey handler to register");
toggleWindow();
assert.equal(showCalls.length, 1);
assert.equal(showCalls[0], win);
assert.equal(appFocusCalls, 1);
assert.equal(win.showCalls, 0, "should not call bare win.show()");
assert.equal(win.focusCalls, 0, "should not call bare win.focus()");
} finally {
require.cache[windowManagerPath].exports = actualWindowManager;
}
});
});
test("openMainWindow delegates to showAndFocusMainWindow on win32", async () => {
await withPlatform("win32", async () => {
const windowManagerPath = require.resolve("./windowManager.cjs");
const actualWindowManager = require(windowManagerPath);
const showCalls = [];
require.cache[windowManagerPath].exports = {
...actualWindowManager,
showAndFocusMainWindow(win) {
showCalls.push(win);
return true;
},
};
try {
const bridge = loadBridge();
const electronModule = createElectronStub();
electronModule.app.focus = () => {};
const win = new FakeWindow();
win.visible = false;
electronModule.BrowserWindow.getAllWindows = () => [win];
const { ipcMain } = await enableCloseToTray(bridge, electronModule, {
getMainWindow: () => win,
});
await ipcMain.handlers.get("netcatty:trayPanel:openMainWindow")();
assert.equal(showCalls.length, 1);
assert.equal(showCalls[0], win);
assert.equal(win.showCalls, 0);
assert.equal(win.focusCalls, 0);
} finally {
require.cache[windowManagerPath].exports = actualWindowManager;
}
});
});
test("toggleWindowVisibility focuses visible-but-unfocused windows via showAndFocusMainWindow", async () => {
await withPlatform("win32", async () => {
const windowManagerPath = require.resolve("./windowManager.cjs");
const actualWindowManager = require(windowManagerPath);
const showCalls = [];
require.cache[windowManagerPath].exports = {
...actualWindowManager,
showAndFocusMainWindow(win) {
showCalls.push(win);
return true;
},
};
try {
const bridge = loadBridge();
const electronModule = createElectronStub();
electronModule.app.focus = () => {};
const win = new FakeWindow();
win.visible = true;
win.focused = false;
electronModule.BrowserWindow.getAllWindows = () => [win];
let toggleWindow = null;
electronModule.globalShortcut.register = (_accelerator, handler) => {
toggleWindow = handler;
return true;
};
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
await ipcMain.handlers.get("netcatty:globalHotkey:register")(null, { hotkey: "Ctrl + `" });
toggleWindow();
assert.equal(showCalls.length, 1);
assert.equal(win.hideCalls, 0);
} finally {
require.cache[windowManagerPath].exports = actualWindowManager;
}
});
});
function installFakeTrayPanelWindow(electronModule, { getBoundsSize } = {}) {
class FakePanelWindow extends EventEmitter {
constructor(opts = {}) {
super();
FakePanelWindow.instances.push(this);
this.opts = opts;
this.bounds = {
x: 0,
y: 0,
width: opts.width,
height: opts.height,
};
this.setBoundsCalls = [];
this.destroyed = false;
this.visible = false;
const webContents = new EventEmitter();
webContents.send = () => {};
this.webContents = webContents;
}
async loadURL() {}
getBounds() {
if (getBoundsSize) return { ...this.bounds, ...getBoundsSize };
return { ...this.bounds };
}
setBounds(next) {
this.setBoundsCalls.push({ ...next });
this.bounds = { ...this.bounds, ...next };
}
isDestroyed() {
return this.destroyed;
}
show() {
this.visible = true;
}
hide() {
this.visible = false;
}
focus() {}
destroy() {
this.destroyed = true;
}
}
FakePanelWindow.instances = [];
FakePanelWindow.getAllWindows = () => [];
electronModule.BrowserWindow = FakePanelWindow;
return FakePanelWindow;
}
test("Windows right-click places the designed panel above the taskbar from event bounds", async () => {
await withPlatform("win32", async () => {
const { placeTrayPanel } = require("./trayPanelBounds.cjs");
const bridge = loadBridge();
const electronModule = createElectronStub();
const FakePanelWindow = installFakeTrayPanelWindow(electronModule, {
getBoundsSize: { width: 720, height: 1040 },
});
const workArea = { x: 0, y: 0, width: 1920, height: 1040 };
const eventBounds = { x: 1680, y: 1044, width: 24, height: 24 };
electronModule.screen = {
getCursorScreenPoint: () => ({ x: 1690, y: 1050 }),
getDisplayNearestPoint: () => ({ workArea }),
};
try {
await enableCloseToTray(bridge, electronModule);
const trayInstance = bridge.getTray();
trayInstance.getBounds = () => ({ x: 0, y: 0, width: 0, height: 0 });
trayInstance.handlers.get("right-click")({}, eventBounds);
assert.equal(FakePanelWindow.instances.length, 1);
assert.equal(FakePanelWindow.instances[0].opts.width, 360);
assert.equal(FakePanelWindow.instances[0].opts.height, 520);
assert.deepEqual(
FakePanelWindow.instances[0].setBoundsCalls[0],
placeTrayPanel({ anchor: eventBounds, workArea, width: 360, height: 520 }),
);
} finally {
bridge.cleanup();
}
});
});
test("Windows right-click ignores a y=0 tray.getBounds lie and uses the cursor", async () => {
await withPlatform("win32", async () => {
const { placeTrayPanel } = require("./trayPanelBounds.cjs");
const bridge = loadBridge();
const electronModule = createElectronStub();
const FakePanelWindow = installFakeTrayPanelWindow(electronModule);
const workArea = { x: 0, y: 0, width: 1920, height: 1040 };
const cursor = { x: 1700, y: 1040 };
electronModule.screen = {
getCursorScreenPoint: () => cursor,
getDisplayNearestPoint: () => ({ workArea }),
};
try {
await enableCloseToTray(bridge, electronModule);
const trayInstance = bridge.getTray();
trayInstance.getBounds = () => ({ x: 1680, y: 0, width: 24, height: 24 });
trayInstance.handlers.get("right-click")({});
assert.deepEqual(
FakePanelWindow.instances[0].setBoundsCalls[0],
placeTrayPanel({
anchor: { x: cursor.x, y: cursor.y, width: 1, height: 1 },
workArea,
width: 360,
height: 520,
}),
);
} finally {
bridge.cleanup();
}
});
});
test("Windows tray activation keeps event bounds when the cursor is on another monitor", async () => {
await withPlatform("win32", async () => {
const { placeTrayPanel } = require("./trayPanelBounds.cjs");
const bridge = loadBridge();
const electronModule = createElectronStub();
const FakePanelWindow = installFakeTrayPanelWindow(electronModule);
const workArea = { x: 0, y: 0, width: 1920, height: 1040 };
const eventBounds = { x: 1680, y: 1044, width: 24, height: 24 };
const nearestCalls = [];
electronModule.screen = {
getCursorScreenPoint: () => ({ x: 2600, y: 400 }),
getDisplayNearestPoint: (point) => {
nearestCalls.push(point);
return { workArea };
},
};
try {
await enableCloseToTray(bridge, electronModule);
const trayInstance = bridge.getTray();
trayInstance.getBounds = () => ({ x: 0, y: 0, width: 0, height: 0 });
trayInstance.handlers.get("right-click")({}, eventBounds);
assert.deepEqual(nearestCalls[0], { x: eventBounds.x, y: eventBounds.y });
assert.deepEqual(
FakePanelWindow.instances[0].setBoundsCalls[0],
placeTrayPanel({ anchor: eventBounds, workArea, width: 360, height: 520 }),
);
} finally {
bridge.cleanup();
}
});
});
test("macOS tray click still opens the panel below a top menu-bar icon", async () => {
await withPlatform("darwin", async () => {
const { placeTrayPanel } = require("./trayPanelBounds.cjs");
const bridge = loadBridge();
const electronModule = createElectronStub();
const FakePanelWindow = installFakeTrayPanelWindow(electronModule);
const workArea = { x: 0, y: 25, width: 1440, height: 875 };
const eventBounds = { x: 900, y: 0, width: 24, height: 24 };
electronModule.screen = {
getCursorScreenPoint: () => ({ x: 910, y: 8 }),
getDisplayNearestPoint: () => ({ workArea }),
};
try {
await enableCloseToTray(bridge, electronModule);
const trayInstance = bridge.getTray();
trayInstance.getBounds = () => eventBounds;
trayInstance.handlers.get("click")({}, eventBounds);
assert.deepEqual(
FakePanelWindow.instances[0].setBoundsCalls[0],
placeTrayPanel({ anchor: eventBounds, workArea, width: 360, height: 520 }),
);
} finally {
bridge.cleanup();
}
});
});
test("pinTrayForHiddenLaunch creates a tray even when close-to-tray is off", () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
bridge.init({ electronModule, getMainWindow: () => null });
try {
assert.equal(bridge.getTray(), null);
bridge.pinTrayForHiddenLaunch();
assert.notEqual(bridge.getTray(), null);
} finally {
bridge.cleanup();
}
});
test("a hidden-launch tray pin survives close-to-tray being turned off", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
bridge.init({ electronModule, getMainWindow: () => null });
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
try {
bridge.pinTrayForHiddenLaunch();
assert.notEqual(bridge.getTray(), null, "hidden-launch cold start must have a tray");
// The renderer's close-to-tray effect runs on mount and can disable the
// preference before the pin is released — the tray must not disappear,
// or a hidden auto-launch becomes an unreachable zombie process.
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: false });
assert.notEqual(bridge.getTray(), null, "the pin must outlive close-to-tray being disabled");
} finally {
bridge.cleanup();
}
});
test("releasing the hidden-launch tray pin destroys the tray if close-to-tray is off", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
bridge.init({ electronModule, getMainWindow: () => null });
const ipcMain = createIpcMainStub();
bridge.registerHandlers(ipcMain);
try {
bridge.pinTrayForHiddenLaunch();
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: false });
assert.notEqual(bridge.getTray(), null);
bridge.releaseHiddenLaunchTrayPin();
assert.equal(bridge.getTray(), null, "once the window is shown, close-to-tray=off should win");
} finally {
bridge.cleanup();
}
});
test("releasing the hidden-launch tray pin keeps the tray if close-to-tray is on", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
bridge.init({ electronModule, getMainWindow: () => null });
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
try {
bridge.pinTrayForHiddenLaunch();
assert.notEqual(bridge.getTray(), null);
bridge.releaseHiddenLaunchTrayPin();
assert.notEqual(bridge.getTray(), null, "close-to-tray=on should keep the tray regardless of the pin");
void ipcMain;
} finally {
bridge.cleanup();
}
});
test("releaseHiddenLaunchTrayPin is a no-op when the pin was never set", () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
bridge.init({ electronModule, getMainWindow: () => null });
try {
assert.doesNotThrow(() => bridge.releaseHiddenLaunchTrayPin());
assert.equal(bridge.getTray(), null);
} finally {
bridge.cleanup();
}
});
test("setCloseToTray(false) still destroys an unpinned tray as before", async () => {
const bridge = loadBridge();
const electronModule = createElectronStub();
const { ipcMain } = await enableCloseToTray(bridge, electronModule);
try {
assert.notEqual(bridge.getTray(), null);
await ipcMain.handlers.get("netcatty:tray:setCloseToTray")(null, { enabled: false });
assert.equal(bridge.getTray(), null);
} finally {
bridge.cleanup();
}
});