mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
feat: Implement application bootstrap and window management
- Added bootstrap functionality to initialize the Electron application. - Created a new paths module to manage application paths and URLs. - Introduced a shutdown service to handle graceful application shutdowns. - Refactored error logging to use a dedicated logger module. - Implemented process killing logic for NodeCG processes across platforms. - Established navigation policies for internal and external URL handling in windows. - Developed window service for creating and managing application windows. - Added tests for application paths, application controller, navigation policies, process killer, and shutdown service.
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { EventEmitter } from "node:events";
|
||||
import test from "node:test";
|
||||
|
||||
import { createApplicationController, ApplicationWindow } from "../main/app/application-controller";
|
||||
import { AppRuntimeConfig } from "../main/config/runtime-config";
|
||||
import { NodecgProcessManager } from "../main/nodecg/process-manager";
|
||||
|
||||
class MockWindow implements ApplicationWindow {
|
||||
private destroyed = false;
|
||||
private minimized = false;
|
||||
|
||||
constructor(
|
||||
private readonly name: string,
|
||||
private readonly events: string[],
|
||||
) {}
|
||||
|
||||
close(): void {
|
||||
this.events.push(`${this.name}:close`);
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.events.push(`${this.name}:focus`);
|
||||
}
|
||||
|
||||
isDestroyed(): boolean {
|
||||
return this.destroyed;
|
||||
}
|
||||
|
||||
isMinimized(): boolean {
|
||||
return this.minimized;
|
||||
}
|
||||
|
||||
async loadURL(url: string): Promise<void> {
|
||||
this.events.push(`${this.name}:load:${url}`);
|
||||
}
|
||||
|
||||
restore(): void {
|
||||
this.events.push(`${this.name}:restore`);
|
||||
this.minimized = false;
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this.events.push(`${this.name}:show`);
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseConfig(): AppRuntimeConfig {
|
||||
return {
|
||||
title: "Scoreko",
|
||||
userModelId: "com.scoreko.desktop",
|
||||
userDataDirectoryName: "scoreko",
|
||||
nodecgPort: "9090",
|
||||
bundleName: "scoreko-dev",
|
||||
mainDashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true",
|
||||
loadingDashboardRoute: "dashboard/loading/main.html?standalone=true",
|
||||
loadDelayMs: 0,
|
||||
startupTimeoutMs: 100,
|
||||
nodecgKillTimeoutMs: 10,
|
||||
updatesEnabled: true,
|
||||
updateAssetPattern: "Scoreko-setup-.*\\.exe$",
|
||||
updateCheckDelayMs: 5000,
|
||||
};
|
||||
}
|
||||
|
||||
function createMockManager(events: string[]): NodecgProcessManager {
|
||||
return {
|
||||
startNodecgProcess: async () => {
|
||||
events.push("start-nodecg");
|
||||
return new EventEmitter() as import("node:child_process").ChildProcess;
|
||||
},
|
||||
waitForNodecgReady: async () => {
|
||||
events.push("wait-nodecg");
|
||||
},
|
||||
stopNodecgProcessGracefully: async () => {
|
||||
events.push("stop-nodecg");
|
||||
},
|
||||
getProcess: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
test("ApplicationController preserves startup ordering and schedules updates after main window is shown", async () => {
|
||||
const events: string[] = [];
|
||||
const paths = {
|
||||
rootPath: "/app",
|
||||
sourceNodecgRuntimePath: "/app/lib/nodecg",
|
||||
userDataPath: "/user-data/scoreko",
|
||||
nodecgBaseUrl: "http://127.0.0.1:9090",
|
||||
mainDashboardUrl: "http://localhost:9090/bundles/scoreko-dev/dashboard/main.html?standalone=true",
|
||||
loadingDashboardUrl: "http://localhost:9090/bundles/scoreko-dev/dashboard/loading/main.html?standalone=true",
|
||||
};
|
||||
|
||||
const controller = createApplicationController({
|
||||
appConfig: getBaseConfig(),
|
||||
appVersion: "0.1.0",
|
||||
isPackaged: false,
|
||||
isWindows: true,
|
||||
paths,
|
||||
deps: {
|
||||
createLoadingWindow: () => {
|
||||
events.push("create-loading");
|
||||
return new MockWindow("loading", events);
|
||||
},
|
||||
createMainWindow: () => {
|
||||
events.push("create-main");
|
||||
return new MockWindow("main", events);
|
||||
},
|
||||
createNodecgProcessManager: () => {
|
||||
events.push("create-manager");
|
||||
return createMockManager(events);
|
||||
},
|
||||
getAllWindows: () => [],
|
||||
log: () => undefined,
|
||||
prepareRuntime: () => {
|
||||
events.push("prepare-runtime");
|
||||
return { runtimePath: "/user-data/scoreko/nodecg", installed: false };
|
||||
},
|
||||
relaunch: () => events.push("relaunch"),
|
||||
scheduleUpdateCheck: () => events.push("schedule-update"),
|
||||
setAppUserModelId: () => events.push("set-app-user-model-id"),
|
||||
exit: (code) => events.push(`exit:${code}`),
|
||||
now: () => 0,
|
||||
sleep: async (ms) => {
|
||||
events.push(`sleep:${ms}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await controller.launch();
|
||||
|
||||
assert.equal(controller.getState(), "ready");
|
||||
assert.deepEqual(events, [
|
||||
"set-app-user-model-id",
|
||||
"prepare-runtime",
|
||||
"create-manager",
|
||||
"create-main",
|
||||
"create-loading",
|
||||
"start-nodecg",
|
||||
"wait-nodecg",
|
||||
`loading:load:${paths.loadingDashboardUrl}`,
|
||||
"loading:show",
|
||||
`main:load:${paths.mainDashboardUrl}`,
|
||||
"main:show",
|
||||
"loading:close",
|
||||
"schedule-update",
|
||||
]);
|
||||
});
|
||||
|
||||
test("ApplicationController relaunches packaged app after runtime install before starting NodeCG", async () => {
|
||||
const events: string[] = [];
|
||||
const controller = createApplicationController({
|
||||
appConfig: getBaseConfig(),
|
||||
appVersion: "0.1.0",
|
||||
isPackaged: true,
|
||||
isWindows: false,
|
||||
paths: {
|
||||
rootPath: "/app",
|
||||
sourceNodecgRuntimePath: "/app/lib/nodecg",
|
||||
userDataPath: "/user-data/scoreko",
|
||||
nodecgBaseUrl: "http://127.0.0.1:9090",
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => {
|
||||
throw new Error("window creation should wait until after relaunch decisions");
|
||||
},
|
||||
createMainWindow: () => {
|
||||
throw new Error("window creation should wait until after relaunch decisions");
|
||||
},
|
||||
createNodecgProcessManager: () => {
|
||||
throw new Error("NodeCG should not start before relaunch");
|
||||
},
|
||||
getAllWindows: () => [],
|
||||
log: (...args) => events.push(String(args[0])),
|
||||
prepareRuntime: () => ({ runtimePath: "/user-data/scoreko/nodecg", installed: true }),
|
||||
relaunch: () => events.push("relaunch"),
|
||||
scheduleUpdateCheck: () => events.push("schedule-update"),
|
||||
setAppUserModelId: () => events.push("set-app-user-model-id"),
|
||||
exit: (code) => events.push(`exit:${code}`),
|
||||
},
|
||||
});
|
||||
|
||||
await controller.launch();
|
||||
|
||||
assert.equal(controller.getState(), "stopped");
|
||||
assert.deepEqual(events, [
|
||||
"Runtime was installed or refreshed; relaunching Scoreko before starting NodeCG.",
|
||||
"relaunch",
|
||||
"exit:0",
|
||||
]);
|
||||
});
|
||||
|
||||
test("ApplicationController activation before readiness routes through launch", async () => {
|
||||
const events: string[] = [];
|
||||
const controller = createApplicationController({
|
||||
appConfig: getBaseConfig(),
|
||||
appVersion: "0.1.0",
|
||||
isPackaged: false,
|
||||
isWindows: false,
|
||||
paths: {
|
||||
rootPath: "/app",
|
||||
sourceNodecgRuntimePath: "/app/lib/nodecg",
|
||||
userDataPath: "/user-data/scoreko",
|
||||
nodecgBaseUrl: "http://127.0.0.1:9090",
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => new MockWindow("loading", events),
|
||||
createMainWindow: () => new MockWindow("main", events),
|
||||
createNodecgProcessManager: () => createMockManager(events),
|
||||
getAllWindows: () => [],
|
||||
log: () => undefined,
|
||||
prepareRuntime: () => {
|
||||
events.push("prepare-runtime");
|
||||
return { runtimePath: "/user-data/scoreko/nodecg", installed: false };
|
||||
},
|
||||
relaunch: () => events.push("relaunch"),
|
||||
scheduleUpdateCheck: () => events.push("schedule-update"),
|
||||
setAppUserModelId: () => events.push("set-app-user-model-id"),
|
||||
exit: (code) => events.push(`exit:${code}`),
|
||||
now: () => 0,
|
||||
},
|
||||
});
|
||||
|
||||
await controller.activate();
|
||||
|
||||
assert.equal(controller.getState(), "ready");
|
||||
assert.ok(events.includes("prepare-runtime"));
|
||||
assert.ok(events.includes("start-nodecg"));
|
||||
assert.ok(events.includes("wait-nodecg"));
|
||||
});
|
||||
|
||||
test("ApplicationController shutdown is idempotent", async () => {
|
||||
const events: string[] = [];
|
||||
const controller = createApplicationController({
|
||||
appConfig: getBaseConfig(),
|
||||
appVersion: "0.1.0",
|
||||
isPackaged: false,
|
||||
isWindows: false,
|
||||
paths: {
|
||||
rootPath: "/app",
|
||||
sourceNodecgRuntimePath: "/app/lib/nodecg",
|
||||
userDataPath: "/user-data/scoreko",
|
||||
nodecgBaseUrl: "http://127.0.0.1:9090",
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => new MockWindow("loading", events),
|
||||
createMainWindow: () => new MockWindow("main", events),
|
||||
createNodecgProcessManager: () => createMockManager(events),
|
||||
getAllWindows: () => [],
|
||||
log: () => undefined,
|
||||
prepareRuntime: () => ({ runtimePath: "/user-data/scoreko/nodecg", installed: false }),
|
||||
relaunch: () => events.push("relaunch"),
|
||||
scheduleUpdateCheck: () => events.push("schedule-update"),
|
||||
setAppUserModelId: () => events.push("set-app-user-model-id"),
|
||||
exit: (code) => events.push(`exit:${code}`),
|
||||
now: () => 0,
|
||||
},
|
||||
});
|
||||
|
||||
await controller.launch();
|
||||
await Promise.all([controller.stopNodecgGracefully(), controller.stopNodecgGracefully()]);
|
||||
|
||||
assert.equal(controller.getState(), "stopped");
|
||||
assert.equal(events.filter((event) => event === "stop-nodecg").length, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user