447 lines
10 KiB
JavaScript
447 lines
10 KiB
JavaScript
|
|
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",
|
||
|
|
]);
|
||
|
|
});
|