[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
158
scripts/tray-panel-layout.live.test.cjs
Normal file
158
scripts/tray-panel-layout.live.test.cjs
Normal file
@@ -0,0 +1,158 @@
|
||||
"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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user