Files
NetMesh/electron/main/appLockLifecycle.test.cjs

447 lines
10 KiB
JavaScript
Raw Normal View History

const test = require("node:test");
const assert = require("node:assert/strict");
const {
emitAppLockReopen,
ensureAppLockForFreshSession,
handleAppHide,
handleActivateWithMainWindow,
handleBeforeQuit,
hasNoUsableAppContentWindows,
shouldBackgroundLockOnHide,
shouldCommitQuitWithoutDirtyCheck,
} = require("./appLockLifecycle.cjs");
test("shouldBackgroundLockOnHide locks only when app lock controller exists", () => {
assert.equal(shouldBackgroundLockOnHide({ setLocked: () => {} }), true);
assert.equal(shouldBackgroundLockOnHide(null), false);
});
test("ensureAppLockForFreshSession locks with startup by default", () => {
const calls = [];
assert.equal(
ensureAppLockForFreshSession({
setLocked(reason) {
calls.push(reason);
},
}),
true,
);
assert.deepEqual(calls, ["startup"]);
});
test("ensureAppLockForFreshSession accepts an explicit reason and ignores missing controllers", () => {
const calls = [];
assert.equal(
ensureAppLockForFreshSession({
setLocked(reason) {
calls.push(reason);
},
}, "background"),
true,
);
assert.deepEqual(calls, ["background"]);
assert.equal(ensureAppLockForFreshSession(null), false);
assert.equal(ensureAppLockForFreshSession({}), false);
});
test("hasNoUsableAppContentWindows treats empty or destroyed lists as no windows", () => {
assert.equal(hasNoUsableAppContentWindows(undefined), true);
assert.equal(hasNoUsableAppContentWindows([]), true);
assert.equal(hasNoUsableAppContentWindows([null, { isDestroyed: () => true }]), true);
assert.equal(hasNoUsableAppContentWindows([{ isDestroyed: () => false }]), false);
assert.equal(hasNoUsableAppContentWindows([{ /* no isDestroyed */ }]), false);
// Hidden prewarm windows do not count as a live session.
assert.equal(
hasNoUsableAppContentWindows([{
isDestroyed: () => false,
isVisible: () => false,
}]),
true,
);
assert.equal(
hasNoUsableAppContentWindows([{
isDestroyed: () => false,
isVisible: () => true,
}]),
false,
);
});
test("shouldCommitQuitWithoutDirtyCheck commits when no reachable main windows exist", () => {
assert.equal(
shouldCommitQuitWithoutDirtyCheck({
reachableMainWindows: [],
queryableWebContents: [{ id: 1 }],
}),
true,
);
});
test("shouldCommitQuitWithoutDirtyCheck commits when no queryable webContents exist", () => {
assert.equal(
shouldCommitQuitWithoutDirtyCheck({
reachableMainWindows: [{ id: 1 }],
queryableWebContents: [],
}),
true,
);
});
test("shouldCommitQuitWithoutDirtyCheck waits for dirty check when reachable renderers exist", () => {
assert.equal(
shouldCommitQuitWithoutDirtyCheck({
reachableMainWindows: [{ id: 1 }],
queryableWebContents: [{ id: 1 }],
}),
false,
);
});
test("emitAppLockReopen sends reopen once per live unique webContents", () => {
const sent = [];
const sharedWebContents = {
id: 7,
send(channel) {
sent.push(channel);
},
};
const windows = [
null,
{ isDestroyed: () => true, webContents: sharedWebContents },
{ isDestroyed: () => false, webContents: sharedWebContents },
{ isDestroyed: () => false, webContents: sharedWebContents },
{
isDestroyed: () => false,
webContents: {
id: 8,
send(channel) {
sent.push(channel);
},
},
},
];
emitAppLockReopen(windows);
assert.deepEqual(sent, [
"netcatty:app-lock:reopen",
"netcatty:app-lock:reopen",
]);
});
test("handleAppHide locks the app in background when controller exists", () => {
const calls = [];
handleAppHide({
setLocked(reason) {
calls.push(reason);
},
});
assert.deepEqual(calls, ["background"]);
});
test("handleAppHide ignores missing controllers", () => {
assert.doesNotThrow(() => {
handleAppHide(null);
});
});
test("handleActivateWithMainWindow shows and focuses the main window, then emits reopen", () => {
const calls = [];
const mainWindow = {
isDestroyed: () => false,
isMinimized: () => true,
restore() {
calls.push("restore");
},
show() {
calls.push("show");
},
focus() {
calls.push("focus");
},
webContents: {
id: 99,
send(channel) {
calls.push(`send:${channel}`);
},
},
};
const app = {
focus() {
calls.push("app.focus");
},
};
const globalShortcutBridge = {
clearPendingFullscreenHide(win) {
calls.push(`clear:${win === mainWindow}`);
},
};
const handled = handleActivateWithMainWindow({
app,
mainWindow,
globalShortcutBridge,
reopenWindows: [mainWindow],
});
assert.equal(handled, true);
assert.deepEqual(calls, [
"clear:true",
"restore",
"show",
"focus",
"send:netcatty:app-lock:reopen",
"app.focus",
]);
});
test("handleActivateWithMainWindow refuses crashed main windows so activate can recreate them", () => {
const calls = [];
const mainWindow = {
isDestroyed: () => false,
destroy() {
calls.push("destroy");
},
webContents: {
isCrashed: () => true,
id: 99,
send(channel) {
calls.push(`send:${channel}`);
},
},
};
const handled = handleActivateWithMainWindow({
app: {
focus() {
calls.push("app.focus");
},
},
mainWindow,
globalShortcutBridge: {
clearPendingFullscreenHide() {
calls.push("clear");
},
},
reopenWindows: [mainWindow],
});
assert.equal(handled, false);
assert.deepEqual(calls, ["destroy"]);
});
test("handleBeforeQuit commits quit after clean dirty-editor check and locks background", async () => {
const calls = [];
const mainWindow = {
isDestroyed: () => false,
isVisible: () => true,
isMinimized: () => false,
webContents: {
isDestroyed: () => false,
isCrashed: () => false,
id: 1,
},
};
const event = {
preventDefault() {
calls.push("preventDefault");
},
};
await handleBeforeQuit({
event,
mainWindows: [mainWindow],
queryDirtyEditors: async () => false,
appLockController: {
setLocked(reason) {
calls.push(`lock:${reason}`);
},
},
windowManager: {
setIsQuitting(value) {
calls.push(`setIsQuitting:${value}`);
},
isQuittingForUpdate() {
return false;
},
},
app: {
quit() {
calls.push("app.quit");
},
},
ipcMain: {},
quitConfirmed: false,
quitGuardChannelBusy: false,
timeoutMs: 10,
setQuitGuardChannelBusy(value) {
calls.push(`quitGuardBusy:${value}`);
},
setQuitConfirmed(value) {
calls.push(`quitConfirmed:${value}`);
},
});
assert.deepEqual(calls, [
"quitGuardBusy:true",
"preventDefault",
"quitGuardBusy:false",
"lock:background",
"setIsQuitting:true",
"quitConfirmed:true",
"app.quit",
]);
});
test("handleBeforeQuit cancels quit without locking when dirty editors exist", async () => {
const calls = [];
const mainWindow = {
isDestroyed: () => false,
isVisible: () => true,
isMinimized: () => false,
webContents: {
isDestroyed: () => false,
isCrashed: () => false,
id: 1,
},
};
const event = {
preventDefault() {
calls.push("preventDefault");
},
};
await handleBeforeQuit({
event,
mainWindows: [mainWindow],
queryDirtyEditors: async () => true,
appLockController: {
setLocked(reason) {
calls.push(`lock:${reason}`);
},
},
windowManager: {
setIsQuitting(value) {
calls.push(`setIsQuitting:${value}`);
},
isQuittingForUpdate() {
return true;
},
setQuittingForUpdate(value) {
calls.push(`setQuittingForUpdate:${value}`);
},
showAndFocusMainWindow(win) {
calls.push(`showAndFocus:${win === mainWindow}`);
},
},
app: {
quit() {
calls.push("app.quit");
},
},
ipcMain: {},
quitConfirmed: false,
quitGuardChannelBusy: false,
timeoutMs: 10,
setQuitGuardChannelBusy(value) {
calls.push(`quitGuardBusy:${value}`);
},
setQuitConfirmed(value) {
calls.push(`quitConfirmed:${value}`);
},
});
assert.deepEqual(calls, [
"quitGuardBusy:true",
"preventDefault",
"quitGuardBusy:false",
"showAndFocus:true",
"setQuittingForUpdate:false",
]);
});
test("handleBeforeQuit focuses only dirty windows when multiple renderers reply", async () => {
const calls = [];
const cleanWindow = {
isDestroyed: () => false,
webContents: {
isDestroyed: () => false,
isCrashed: () => false,
id: 1,
},
};
const dirtyWindow = {
isDestroyed: () => false,
isMinimized: () => true,
restore() {
calls.push("restore");
},
show() {
calls.push("show");
},
focus() {
calls.push("focus");
},
webContents: {
isDestroyed: () => false,
isCrashed: () => false,
id: 2,
},
};
const event = {
preventDefault() {
calls.push("preventDefault");
},
};
const result = await handleBeforeQuit({
event,
mainWindows: [cleanWindow, dirtyWindow],
queryDirtyEditors: async (wc) => wc.id === 2,
appLockController: {
setLocked(reason) {
calls.push(`lock:${reason}`);
},
},
windowManager: {
// No showAndFocusMainWindow — exercise the restore/show/focus fallback
// used when the window manager helper is unavailable.
setIsQuitting(value) {
calls.push(`setIsQuitting:${value}`);
},
isQuittingForUpdate() {
return false;
},
},
app: {
quit() {
calls.push("app.quit");
},
},
ipcMain: {},
quitConfirmed: false,
quitGuardChannelBusy: false,
timeoutMs: 10,
setQuitGuardChannelBusy(value) {
calls.push(`quitGuardBusy:${value}`);
},
setQuitConfirmed(value) {
calls.push(`quitConfirmed:${value}`);
},
});
assert.equal(result.committed, false);
assert.equal(result.skipped, "dirty");
assert.deepEqual(calls, [
"quitGuardBusy:true",
"preventDefault",
"quitGuardBusy:false",
"restore",
"show",
"focus",
]);
});