mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
test(main): completar fase 2 con cobertura de iconos y timing
This commit is contained in:
+2
-1
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { getRuntimeConfig } from "./config/runtime-config";
|
||||
import { showFatalError, log } from "./errors/error-presenter";
|
||||
import { createNodecgProcessManager } from "./nodecg/process-manager";
|
||||
import { getRemainingDelayMs } from "./utils/timing";
|
||||
import { createLoadingWindow, createMainWindow } from "./windows/window-factory";
|
||||
|
||||
const runtimeConfig = getRuntimeConfig();
|
||||
@@ -50,7 +51,7 @@ async function launch(): Promise<void> {
|
||||
|
||||
await mainWindow.loadURL(dashboardUrl);
|
||||
|
||||
const remainingLoadingDelay = Math.max(0, runtimeConfig.loadDelayMs - (Date.now() - loadingShownAt));
|
||||
const remainingLoadingDelay = getRemainingDelayMs(runtimeConfig.loadDelayMs, loadingShownAt);
|
||||
if (remainingLoadingDelay > 0) {
|
||||
await sleep(remainingLoadingDelay);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function getRemainingDelayMs(targetDelayMs: number, startedAtMs: number, currentTimeMs: number = Date.now()): number {
|
||||
return Math.max(0, targetDelayMs - (currentTimeMs - startedAtMs));
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||
|
||||
export function resolveAppIconPath(
|
||||
runtimeConfig: AppRuntimeConfig,
|
||||
rootPath: string,
|
||||
pathExists: (candidatePath: string) => boolean = fs.existsSync,
|
||||
): string | undefined {
|
||||
const iconCandidates = [
|
||||
runtimeConfig.iconPathOverride,
|
||||
path.join(rootPath, "static", "icons", "icon.ico"),
|
||||
path.join(rootPath, "static", "icons", "icon.png"),
|
||||
path.join(rootPath, "static", "icon.ico"),
|
||||
path.join(rootPath, "static", "icon.png"),
|
||||
].filter((candidate): candidate is string => Boolean(candidate));
|
||||
|
||||
return iconCandidates.find((candidate) => pathExists(candidate));
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, shell } from "electron";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { DEFAULT_WINDOW_BACKGROUND, DEFAULT_WINDOW_SIZE, LOADING_WINDOW_SIZE } from "../constants";
|
||||
import { resolveAppIconPath } from "./icon-path";
|
||||
|
||||
type WindowFactoryDependencies = {
|
||||
runtimeConfig: AppRuntimeConfig;
|
||||
@@ -90,15 +88,3 @@ function createWindowOptions({
|
||||
minHeight: DEFAULT_WINDOW_SIZE.minHeight,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAppIconPath(runtimeConfig: AppRuntimeConfig, rootPath: string): string | undefined {
|
||||
const iconCandidates = [
|
||||
runtimeConfig.iconPathOverride,
|
||||
path.join(rootPath, "static", "icons", "icon.ico"),
|
||||
path.join(rootPath, "static", "icons", "icon.png"),
|
||||
path.join(rootPath, "static", "icon.ico"),
|
||||
path.join(rootPath, "static", "icon.png"),
|
||||
].filter((candidate): candidate is string => Boolean(candidate));
|
||||
|
||||
return iconCandidates.find((candidate) => fs.existsSync(candidate));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { AppRuntimeConfig } from "../main/config/runtime-config";
|
||||
import { resolveAppIconPath } from "../main/windows/icon-path";
|
||||
|
||||
function getBaseConfig(): AppRuntimeConfig {
|
||||
return {
|
||||
title: "Scoreko",
|
||||
userModelId: "com.scoreko.desktop",
|
||||
nodecgPort: "9090",
|
||||
bundleName: "scoreko-dev",
|
||||
dashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true",
|
||||
loadingRoute: "dashboard/loading/main.html?standalone=true",
|
||||
loadDelayMs: 10000,
|
||||
startupTimeoutMs: 30000,
|
||||
nodecgKillTimeoutMs: 2500,
|
||||
};
|
||||
}
|
||||
|
||||
test("resolveAppIconPath prioriza iconPathOverride cuando existe", () => {
|
||||
const runtimeConfig: AppRuntimeConfig = {
|
||||
...getBaseConfig(),
|
||||
iconPathOverride: "/custom/icon.ico",
|
||||
};
|
||||
|
||||
const iconPath = resolveAppIconPath(runtimeConfig, "/app", (candidate) => candidate === "/custom/icon.ico");
|
||||
|
||||
assert.equal(iconPath, "/custom/icon.ico");
|
||||
});
|
||||
|
||||
test("resolveAppIconPath cae al primer icono por defecto existente", () => {
|
||||
const runtimeConfig = getBaseConfig();
|
||||
|
||||
const iconPath = resolveAppIconPath(runtimeConfig, "/app", (candidate) => candidate === "/app/static/icons/icon.png");
|
||||
|
||||
assert.equal(iconPath, "/app/static/icons/icon.png");
|
||||
});
|
||||
|
||||
test("resolveAppIconPath devuelve undefined cuando no hay iconos", () => {
|
||||
const runtimeConfig = getBaseConfig();
|
||||
|
||||
const iconPath = resolveAppIconPath(runtimeConfig, "/app", () => false);
|
||||
|
||||
assert.equal(iconPath, undefined);
|
||||
});
|
||||
@@ -122,3 +122,71 @@ test("stopNodeCG envía SIGTERM y luego SIGKILL si el proceso no sale", async ()
|
||||
child.emit("exit", 0, null);
|
||||
await stopPromise;
|
||||
});
|
||||
|
||||
|
||||
test("stopNodeCG reutiliza la misma promesa cuando se invoca en paralelo", async () => {
|
||||
const child = new MockChildProcess(5555);
|
||||
|
||||
const manager = createNodecgProcessManager({
|
||||
isDev: true,
|
||||
nodecgPath: "/fake/nodecg",
|
||||
baseUrl: "http://127.0.0.1:9090",
|
||||
runtimeConfig: getBaseConfig(),
|
||||
log: () => undefined,
|
||||
deps: {
|
||||
pathExists: () => true,
|
||||
spawnProcess: () => child as unknown as import("node:child_process").ChildProcess,
|
||||
fetchUrl: async () => ({ ok: false, status: 404 } as Response),
|
||||
killProcess: () => undefined,
|
||||
setTimer: () => 0,
|
||||
stdoutWrite: () => undefined,
|
||||
stderrWrite: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
manager.startNodeCG();
|
||||
const firstStop = manager.stopNodeCG();
|
||||
const secondStop = manager.stopNodeCG();
|
||||
|
||||
assert.equal(firstStop, secondStop);
|
||||
|
||||
child.emit("exit", 0, null);
|
||||
await firstStop;
|
||||
});
|
||||
|
||||
test("stopNodeCG normaliza timeout negativo a cero", async () => {
|
||||
const child = new MockChildProcess(7777);
|
||||
const timeouts: number[] = [];
|
||||
|
||||
const manager = createNodecgProcessManager({
|
||||
isDev: true,
|
||||
nodecgPath: "/fake/nodecg",
|
||||
baseUrl: "http://127.0.0.1:9090",
|
||||
runtimeConfig: {
|
||||
...getBaseConfig(),
|
||||
nodecgKillTimeoutMs: -10,
|
||||
},
|
||||
log: () => undefined,
|
||||
deps: {
|
||||
pathExists: () => true,
|
||||
spawnProcess: () => child as unknown as import("node:child_process").ChildProcess,
|
||||
fetchUrl: async () => ({ ok: false, status: 404 } as Response),
|
||||
killProcess: () => undefined,
|
||||
setTimer: (handler, timeoutMs) => {
|
||||
timeouts.push(timeoutMs);
|
||||
handler();
|
||||
return 0;
|
||||
},
|
||||
stdoutWrite: () => undefined,
|
||||
stderrWrite: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
manager.startNodeCG();
|
||||
const stopPromise = manager.stopNodeCG();
|
||||
|
||||
assert.ok(timeouts.includes(0));
|
||||
|
||||
child.emit("exit", 0, null);
|
||||
await stopPromise;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { getRemainingDelayMs } from "../main/utils/timing";
|
||||
|
||||
test("getRemainingDelayMs devuelve el tiempo restante cuando aún no se cumple", () => {
|
||||
const remaining = getRemainingDelayMs(10000, 1000, 4000);
|
||||
assert.equal(remaining, 7000);
|
||||
});
|
||||
|
||||
test("getRemainingDelayMs devuelve 0 si ya pasó el delay", () => {
|
||||
const remaining = getRemainingDelayMs(1000, 1000, 5000);
|
||||
assert.equal(remaining, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user