test(startup): cover window initialization lifecycle

Extract the startup window boundary so hidden creation, ready-to-show ordering, single display, and load rejection handling are verified without Electron UI timing.
This commit is contained in:
Administrator 2026-08-07 17:29:16 +02:00
parent cfeb01e82a
commit b075961802
3 changed files with 79 additions and 9 deletions

View File

@ -2,4 +2,18 @@ function configureStartupRenderer(app) {
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
} }
module.exports = { configureStartupRenderer }; function createStartupWindow(BrowserWindow, options) {
const window = new BrowserWindow({ ...options, show: false });
window.once('ready-to-show', () => {
window.show();
});
return {
window,
load(target, onLoadError) {
return window.loadFile(target).catch(onLoadError);
}
};
}
module.exports = { configureStartupRenderer, createStartupWindow };

11
main.js
View File

@ -1,7 +1,7 @@
process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8'; process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8';
const { monitorEventLoopDelay, PerformanceObserver } = require('perf_hooks'); const { monitorEventLoopDelay, PerformanceObserver } = require('perf_hooks');
const { app, BrowserWindow, ipcMain, dialog, clipboard, nativeTheme, Tray, Menu, nativeImage } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, clipboard, nativeTheme, Tray, Menu, nativeImage } = require('electron');
const { configureStartupRenderer } = require('./lib/startup-renderer'); const { configureStartupRenderer, createStartupWindow } = require('./lib/startup-renderer');
configureStartupRenderer(app); configureStartupRenderer(app);
nativeTheme.themeSource = 'dark'; nativeTheme.themeSource = 'dark';
const path = require('path'); const path = require('path');
@ -1222,13 +1222,12 @@ async function runHosterHealthCheck(config, requestedChecks) {
} }
function createWindow() { function createWindow() {
mainWindow = new BrowserWindow({ const startupWindow = createStartupWindow(BrowserWindow, {
width: 1100, width: 1100,
height: 750, height: 750,
minWidth: 800, minWidth: 800,
minHeight: 550, minHeight: 550,
backgroundColor: '#16181c', backgroundColor: '#16181c',
show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
contextIsolation: true, contextIsolation: true,
@ -1236,11 +1235,9 @@ function createWindow() {
preload: path.join(__dirname, 'preload.js') preload: path.join(__dirname, 'preload.js')
} }
}); });
mainWindow = startupWindow.window;
mainWindow.webContents.setBackgroundThrottling(false); mainWindow.webContents.setBackgroundThrottling(false);
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
mainWindow.webContents.on('render-process-gone', (_event, details) => { mainWindow.webContents.on('render-process-gone', (_event, details) => {
_writeCrashLog('RENDER PROCESS GONE', new Error(details.reason || 'unknown'), details); _writeCrashLog('RENDER PROCESS GONE', new Error(details.reason || 'unknown'), details);
@ -1286,7 +1283,7 @@ function createWindow() {
debugLog(`CHILD PROCESS GONE: type=${details.type} reason=${details.reason} exitCode=${details.exitCode}`); debugLog(`CHILD PROCESS GONE: type=${details.type} reason=${details.reason} exitCode=${details.exitCode}`);
}); });
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html')).catch((err) => { startupWindow.load(path.join(__dirname, 'renderer', 'index.html'), (err) => {
_writeCrashLog('LOAD FILE FAILED', err); _writeCrashLog('LOAD FILE FAILED', err);
debugLog(`LOAD FILE FAILED: ${err && err.stack ? err.stack : err}`); debugLog(`LOAD FILE FAILED: ${err && err.stack ? err.stack : err}`);
}); });

View File

@ -1,9 +1,68 @@
const test = require('node:test'); const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const { configureStartupRenderer } = require('../lib/startup-renderer'); const { EventEmitter } = require('node:events');
const { configureStartupRenderer, createStartupWindow } = require('../lib/startup-renderer');
class TestBrowserWindow extends EventEmitter {
constructor(options) {
super();
this.options = options;
this.showCalls = 0;
this.startupEvents = [];
this.loadError = new Error('renderer load failed');
}
once(eventName, listener) {
this.startupEvents.push(`listen:${eventName}`);
return super.once(eventName, listener);
}
show() {
this.showCalls++;
}
loadFile(target) {
this.startupEvents.push(`load:${target}`);
return Promise.reject(this.loadError);
}
}
test('configureStartupRenderer disables hardware acceleration', () => { test('configureStartupRenderer disables hardware acceleration', () => {
let calls = 0; let calls = 0;
configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }); configureStartupRenderer({ disableHardwareAcceleration() { calls++; } });
assert.equal(calls, 1); assert.equal(calls, 1);
}); });
test('createStartupWindow forces the main window to start hidden', () => {
const startup = createStartupWindow(TestBrowserWindow, { width: 1100, show: true });
assert.equal(startup.window.options.width, 1100);
assert.equal(startup.window.options.show, false);
});
test('startup load registers visibility before navigation and shows only once', async () => {
const startup = createStartupWindow(TestBrowserWindow, {});
const loading = startup.load('renderer/index.html', () => {});
assert.deepEqual(startup.window.startupEvents, [
'listen:ready-to-show',
'load:renderer/index.html'
]);
startup.window.emit('ready-to-show');
startup.window.emit('ready-to-show');
await loading;
assert.equal(startup.window.showCalls, 1);
});
test('startup load forwards a rejected navigation to the error handler', async () => {
const startup = createStartupWindow(TestBrowserWindow, {});
let handledError;
await startup.load('renderer/index.html', (err) => {
handledError = err;
});
assert.equal(handledError, startup.window.loadError);
});