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
235 lines
7.4 KiB
JavaScript
235 lines
7.4 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const { getReusableMainWindow } = require("./mainWindowReuse.cjs");
|
|
|
|
test("getReusableMainWindow returns the tracked healthy main window", () => {
|
|
const win = {
|
|
isDestroyed() {
|
|
return false;
|
|
},
|
|
webContents: {
|
|
isCrashed() {
|
|
return false;
|
|
},
|
|
},
|
|
};
|
|
|
|
assert.equal(
|
|
getReusableMainWindow({
|
|
getWindowManager: () => ({
|
|
getMainWindow: () => win,
|
|
}),
|
|
}),
|
|
win,
|
|
);
|
|
});
|
|
|
|
test("getReusableMainWindow uses the window manager health check before reusing", () => {
|
|
const checked = [];
|
|
const win = {
|
|
isDestroyed() {
|
|
return false;
|
|
},
|
|
webContents: {
|
|
isCrashed() {
|
|
return false;
|
|
},
|
|
},
|
|
};
|
|
|
|
assert.equal(
|
|
getReusableMainWindow({
|
|
getWindowManager: () => ({
|
|
getMainWindow: () => win,
|
|
isWindowUsable(candidate) {
|
|
checked.push(candidate);
|
|
return true;
|
|
},
|
|
}),
|
|
}),
|
|
win,
|
|
);
|
|
assert.deepEqual(checked, [win]);
|
|
});
|
|
|
|
test("getReusableMainWindow ignores windows rejected by the window manager health check", () => {
|
|
const win = {
|
|
isDestroyed() {
|
|
return false;
|
|
},
|
|
webContents: {
|
|
isCrashed() {
|
|
return false;
|
|
},
|
|
},
|
|
};
|
|
|
|
assert.equal(
|
|
getReusableMainWindow({
|
|
getWindowManager: () => ({
|
|
getMainWindow: () => win,
|
|
isWindowUsable() {
|
|
return false;
|
|
},
|
|
}),
|
|
}),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("getReusableMainWindow ignores destroyed windows", () => {
|
|
const win = {
|
|
isDestroyed() {
|
|
return true;
|
|
},
|
|
};
|
|
|
|
assert.equal(
|
|
getReusableMainWindow({
|
|
getWindowManager: () => ({
|
|
getMainWindow: () => win,
|
|
}),
|
|
}),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("getReusableMainWindow destroys and ignores crashed windows", () => {
|
|
const calls = [];
|
|
const win = {
|
|
isDestroyed() {
|
|
return false;
|
|
},
|
|
destroy() {
|
|
calls.push("destroy");
|
|
},
|
|
webContents: {
|
|
isCrashed() {
|
|
return true;
|
|
},
|
|
},
|
|
};
|
|
|
|
assert.equal(
|
|
getReusableMainWindow({
|
|
getWindowManager: () => ({
|
|
getMainWindow: () => win,
|
|
}),
|
|
logWarn: (...args) => calls.push(["warn", ...args]),
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(calls[0][0], "warn");
|
|
assert.deepEqual(calls.slice(1), ["destroy"]);
|
|
});
|
|
|
|
test("getReusableMainWindow returns null when the window manager is unavailable", () => {
|
|
assert.equal(
|
|
getReusableMainWindow({
|
|
getWindowManager: () => {
|
|
throw new Error("not ready");
|
|
},
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(getReusableMainWindow(), null);
|
|
});
|
|
|
|
test("createAndShowMainWindow reuses a main window before creating another one", () => {
|
|
const source = fs.readFileSync(path.join(__dirname, "main.cjs"), "utf8");
|
|
const functionStart = source.indexOf("async function createAndShowMainWindow()");
|
|
const reuseIndex = source.indexOf("const existingWin = getReusableMainWindow", functionStart);
|
|
const createIndex = source.indexOf("mainWindowStartupPromise = (async () =>", functionStart);
|
|
|
|
assert.notEqual(functionStart, -1);
|
|
assert.ok(reuseIndex > functionStart);
|
|
assert.ok(createIndex > functionStart);
|
|
assert.ok(reuseIndex < createIndex);
|
|
assert.match(source.slice(reuseIndex, createIndex), /return existingWin;/);
|
|
});
|
|
|
|
test("a successful cold start always marks processErrorController runtime protection usable, hidden or not", () => {
|
|
// hasShownMainWindow (see processErrorGuards.cjs) is a one-way latch: once
|
|
// any completeMainWindowStartup({windowShown:true}) call happens, runtime
|
|
// protection stays active (non-network errors stop being treated as fatal
|
|
// startup failures) for the rest of the process's life. A hidden auto-launch
|
|
// cold start still successfully creates and loads the window — it is a
|
|
// deliberate, completed startup, not a failure — so the success branch must
|
|
// report windowShown:true unconditionally rather than `!startHidden`, or a
|
|
// tray-only session would never leave "strict" mode and any later
|
|
// non-network error would be fatal.
|
|
const source = fs.readFileSync(path.join(__dirname, "main.cjs"), "utf8");
|
|
const functionStart = source.indexOf("async function createAndShowMainWindow()");
|
|
const tryStart = source.indexOf("const win = await createWindow({ startHidden });", functionStart);
|
|
const catchStart = source.indexOf("} catch (err) {", tryStart);
|
|
const finallyStart = source.indexOf("} finally {", catchStart);
|
|
|
|
assert.notEqual(functionStart, -1);
|
|
assert.ok(tryStart > functionStart);
|
|
assert.ok(catchStart > tryStart);
|
|
assert.ok(finallyStart > catchStart);
|
|
|
|
const successBranch = source.slice(tryStart, catchStart);
|
|
assert.match(
|
|
successBranch,
|
|
/completeMainWindowStartup\(\{\s*windowShown:\s*true\s*\}\)/,
|
|
"success path must not gate windowShown on !startHidden",
|
|
);
|
|
assert.doesNotMatch(successBranch, /windowShown:\s*!startHidden/);
|
|
|
|
const catchBranch = source.slice(catchStart, finallyStart);
|
|
assert.match(
|
|
catchBranch,
|
|
/completeMainWindowStartup\(\{\s*windowShown:\s*false\s*\}\)/,
|
|
"a startup failure (createWindow threw) must still leave protection strict",
|
|
);
|
|
});
|
|
|
|
test("foreground-triggered createAndShowMainWindow call sites re-focus after creation", () => {
|
|
// consumeColdStartHiddenLaunch() is a one-time latch consumed by whichever
|
|
// createAndShowMainWindow() call reaches it first, which is not guaranteed
|
|
// to be the default bootstrap call: a genuine second instance or Dock
|
|
// reopen can race ahead of it (arriving after app.whenReady() but before
|
|
// the bootstrap's own await chain gets there). If a foreground-triggered
|
|
// call happens to consume the --hidden flag, it must still end up visible,
|
|
// or a user-initiated relaunch/reopen silently does nothing. The four deep
|
|
// link / open-terminal-path delivery functions already call
|
|
// focusMainWindow() unconditionally after awaiting createAndShowMainWindow()
|
|
// (verified separately by exercising those functions); this test covers
|
|
// the three fire-and-forget call sites that don't await it directly.
|
|
const source = fs.readFileSync(path.join(__dirname, "main.cjs"), "utf8");
|
|
const foregroundCallSites = [
|
|
{
|
|
name: "second-instance / open-terminal-path invalid-path fallback",
|
|
anchor: "Failed to recreate window on open-terminal-path",
|
|
},
|
|
{
|
|
name: "second-instance / plain fallback",
|
|
anchor: "Failed to recreate window on second-instance",
|
|
},
|
|
{
|
|
name: "activate handler (Dock reopen/click)",
|
|
anchor: "Failed to create window on activate",
|
|
},
|
|
];
|
|
|
|
for (const { name, anchor } of foregroundCallSites) {
|
|
const anchorIndex = source.indexOf(anchor);
|
|
assert.notEqual(anchorIndex, -1, `expected to find call site: ${name}`);
|
|
// The .then(() => { focusMainWindow(); }) must appear on the same
|
|
// createAndShowMainWindow() call, i.e. between the nearest preceding
|
|
// "void createAndShowMainWindow()" and this catch handler's error text.
|
|
const callStart = source.lastIndexOf("void createAndShowMainWindow()", anchorIndex);
|
|
assert.ok(callStart > -1 && callStart < anchorIndex, `expected a createAndShowMainWindow() call before: ${name}`);
|
|
const callSlice = source.slice(callStart, anchorIndex);
|
|
assert.match(
|
|
callSlice,
|
|
/\.then\(\(\) => \{\s*focusMainWindow\(\);\s*\}\)/,
|
|
`${name} must re-focus after creation to guarantee visibility regardless of which flag it raced to consume`,
|
|
);
|
|
}
|
|
});
|