Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
159 lines
7.4 KiB
JavaScript
159 lines
7.4 KiB
JavaScript
"use strict";
|
|
|
|
if (!process.versions.electron) {
|
|
require("node:test")("tray panel fills its window through the app-lock gate", {
|
|
skip: "run npm run build && npm run test:tray-panel-layout",
|
|
}, () => {});
|
|
} else {
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { app, BrowserWindow, ipcMain, screen } = require("electron");
|
|
const tempDirBridge = require("../electron/bridges/tempDirBridge.cjs");
|
|
const { TRAY_PANEL_WIDTH, TRAY_PANEL_HEIGHT, placeTrayPanel } = require("../electron/bridges/trayPanelBounds.cjs");
|
|
const { windowsCssRoundedOverlayChromeOptions } = require("../electron/bridges/windowManager/windowsWindowChrome.cjs");
|
|
|
|
const userData = fs.mkdtempSync(`${tempDirBridge.getTempFilePath("tray-layout-test")}-`);
|
|
app.setPath("userData", userData);
|
|
app.on("window-all-closed", () => {});
|
|
const scale = process.env.NETCATTY_TRAY_LAYOUT_SCALE || "1";
|
|
app.commandLine.appendSwitch("force-device-scale-factor", scale);
|
|
let win;
|
|
let panelBounds;
|
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const finish = (code) => {
|
|
clearTimeout(watchdog);
|
|
if (win && !win.isDestroyed()) win.destroy();
|
|
try {
|
|
fs.rmSync(userData, { recursive: true, force: true });
|
|
} catch (error) {
|
|
console.warn("Unable to remove tray layout test data:", error.message);
|
|
}
|
|
app.exit(code);
|
|
};
|
|
const watchdog = setTimeout(() => {
|
|
console.error("TRAY_LAYOUT_FAIL timed out");
|
|
finish(1);
|
|
}, 30_000);
|
|
|
|
// Only the desktop service boundary is faked. Load the complete built tray
|
|
// route, including AppLockGate, providers, and the production styles.
|
|
const preload = path.join(userData, "preload.cjs");
|
|
fs.writeFileSync(preload, `
|
|
const { contextBridge, ipcRenderer } = require('electron');
|
|
let state = { initialized: true, locked: false, reason: null, version: 1 };
|
|
const listen = (channel, callback) => {
|
|
const listener = (_event, value) => callback(value);
|
|
ipcRenderer.on(channel, listener);
|
|
return () => ipcRenderer.removeListener(channel, listener);
|
|
};
|
|
listen('test:lock-state', next => { state = next; });
|
|
contextBridge.exposeInMainWorld('netcatty', {
|
|
getAppLockRuntimeState: async () => state,
|
|
onAppLockRuntimeStateChanged: callback => listen('test:lock-state', callback),
|
|
onTrayPanelMenuData: callback => listen('test:menu-data', callback),
|
|
hideTrayPanel: () => ipcRenderer.send('test:hide-tray'),
|
|
});
|
|
`);
|
|
|
|
const waitFor = async (expression) => {
|
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
if (await win.webContents.executeJavaScript(expression)) return;
|
|
await delay(50);
|
|
}
|
|
assert.fail(`Timed out waiting for ${expression}`);
|
|
};
|
|
const checkLayout = async (label, locked = false, expectedBounds = panelBounds) => {
|
|
const actual = await win.webContents.executeJavaScript(`(() => {
|
|
const panel = document.getElementById('tray-panel-root');
|
|
const bounds = panel.getBoundingClientRect();
|
|
const background = document.querySelector('[data-app-lock-background]');
|
|
return {
|
|
width: bounds.width, height: bounds.height,
|
|
viewportWidth: innerWidth, viewportHeight: innerHeight, deviceScale: devicePixelRatio,
|
|
inert: background.inert, hidden: background.getAttribute('aria-hidden'),
|
|
};
|
|
})()`);
|
|
const detail = `${label}: ${JSON.stringify({ ...actual, expectedBounds, nativeBounds: win.getContentBounds() })}`;
|
|
assert.equal(actual.viewportWidth, expectedBounds.width, detail);
|
|
assert.equal(actual.viewportHeight, expectedBounds.height, detail);
|
|
if (process.platform === "win32") assert.equal(actual.deviceScale, Number(scale), label);
|
|
assert.equal(actual.width, actual.viewportWidth, detail);
|
|
assert.equal(actual.height, actual.viewportHeight, `${label}: panel must fill the window, got ${JSON.stringify(actual)}`);
|
|
assert.equal(actual.inert, locked, `${label}: background interaction guard`);
|
|
assert.equal(actual.hidden, locked ? "true" : null, `${label}: accessibility guard`);
|
|
console.log(`TRAY_LAYOUT_PASS ${label} scale=${actual.deviceScale} ${actual.width}x${actual.height}`);
|
|
};
|
|
|
|
void app.whenReady().then(async () => {
|
|
const { workArea, bounds, scaleFactor } = screen.getPrimaryDisplay();
|
|
console.log("TRAY_LAYOUT_DISPLAY", JSON.stringify({ workArea, bounds, scaleFactor }));
|
|
panelBounds = placeTrayPanel({
|
|
anchor: { x: workArea.x + workArea.width - 24, y: workArea.y + workArea.height, width: 24, height: 24 },
|
|
workArea,
|
|
width: TRAY_PANEL_WIDTH,
|
|
height: TRAY_PANEL_HEIGHT,
|
|
});
|
|
win = new BrowserWindow({
|
|
width: TRAY_PANEL_WIDTH,
|
|
height: TRAY_PANEL_HEIGHT,
|
|
show: false,
|
|
frame: false,
|
|
resizable: false,
|
|
...windowsCssRoundedOverlayChromeOptions(),
|
|
webPreferences: { preload, contextIsolation: true, sandbox: false, zoomFactor: 1 },
|
|
});
|
|
ipcMain.on("test:hide-tray", () => win.hide());
|
|
// Match showTrayPanel, including its intentional size limit when the
|
|
// display work area cannot fit the full 360x520 panel (e.g. at 200% scale).
|
|
win.setBounds(panelBounds, false);
|
|
await win.loadFile(path.join(__dirname, "../dist/index.html"), { hash: "/tray" });
|
|
await waitFor("Boolean(document.getElementById('tray-panel-root'))");
|
|
win.show();
|
|
await checkLayout("empty-first-open");
|
|
|
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
win.hide();
|
|
win.setBounds(panelBounds, false);
|
|
win.show();
|
|
await checkLayout(`reopen-${attempt}`);
|
|
}
|
|
|
|
const sessions = Array.from({ length: 30 }, (_, i) => ({
|
|
id: `test-${i}`, label: `Test server ${i}`, hostLabel: `Test server ${i}`, status: "connected",
|
|
}));
|
|
win.webContents.send("test:menu-data", { sessions });
|
|
await waitFor("document.getElementById('tray-panel-root').textContent.includes('Test server 29')");
|
|
await checkLayout("many-sessions");
|
|
|
|
win.webContents.send("test:lock-state", { initialized: true, locked: true, reason: "manual", version: 2 });
|
|
await waitFor("document.querySelector('[data-app-lock-background]').inert");
|
|
await checkLayout("locked", true);
|
|
win.webContents.send("test:lock-state", { initialized: true, locked: false, reason: null, version: 3 });
|
|
await waitFor("!document.querySelector('[data-app-lock-background]').inert");
|
|
await checkLayout("unlocked");
|
|
|
|
win.webContents.send("test:menu-data", { sessions: [] });
|
|
await waitFor("!document.getElementById('tray-panel-root').textContent.includes('Test server 29')");
|
|
await checkLayout("empty-again");
|
|
|
|
// Also exercise a short work area on machines with a large desktop. The
|
|
// original regression leaves an empty panel at 250px, even in a 340px window.
|
|
const shortBounds = { ...panelBounds, height: Math.min(panelBounds.height, 340) };
|
|
win.hide();
|
|
win.setBounds(shortBounds, false);
|
|
win.show();
|
|
await waitFor(`innerHeight === ${shortBounds.height}`);
|
|
await checkLayout("short-work-area", false, shortBounds);
|
|
|
|
win.webContents.sendInputEvent({ type: "keyDown", keyCode: "Escape" });
|
|
win.webContents.sendInputEvent({ type: "keyUp", keyCode: "Escape" });
|
|
for (let attempt = 0; attempt < 50 && win.isVisible(); attempt++) await delay(20);
|
|
assert.equal(win.isVisible(), false, "Escape closes the panel");
|
|
finish(0);
|
|
}).catch((error) => {
|
|
console.error("TRAY_LAYOUT_FAIL", error);
|
|
finish(1);
|
|
});
|
|
}
|