From b0759618022bf41b3250f51dec92f7bb00e45347 Mon Sep 17 00:00:00 2001 From: Administrator Date: Fri, 7 Aug 2026 17:29:16 +0200 Subject: [PATCH] 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. --- lib/startup-renderer.js | 16 ++++++++- main.js | 11 +++--- tests/startup-renderer.test.js | 61 +++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/lib/startup-renderer.js b/lib/startup-renderer.js index 7b3a90a..deded51 100644 --- a/lib/startup-renderer.js +++ b/lib/startup-renderer.js @@ -2,4 +2,18 @@ function configureStartupRenderer(app) { 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 }; diff --git a/main.js b/main.js index 6331fd1..38df739 100644 --- a/main.js +++ b/main.js @@ -1,7 +1,7 @@ process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8'; const { monitorEventLoopDelay, PerformanceObserver } = require('perf_hooks'); 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); nativeTheme.themeSource = 'dark'; const path = require('path'); @@ -1222,13 +1222,12 @@ async function runHosterHealthCheck(config, requestedChecks) { } function createWindow() { - mainWindow = new BrowserWindow({ + const startupWindow = createStartupWindow(BrowserWindow, { width: 1100, height: 750, minWidth: 800, minHeight: 550, backgroundColor: '#16181c', - show: false, autoHideMenuBar: true, webPreferences: { contextIsolation: true, @@ -1236,11 +1235,9 @@ function createWindow() { preload: path.join(__dirname, 'preload.js') } }); + mainWindow = startupWindow.window; mainWindow.webContents.setBackgroundThrottling(false); - mainWindow.once('ready-to-show', () => { - mainWindow.show(); - }); mainWindow.webContents.on('render-process-gone', (_event, 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}`); }); - mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html')).catch((err) => { + startupWindow.load(path.join(__dirname, 'renderer', 'index.html'), (err) => { _writeCrashLog('LOAD FILE FAILED', err); debugLog(`LOAD FILE FAILED: ${err && err.stack ? err.stack : err}`); }); diff --git a/tests/startup-renderer.test.js b/tests/startup-renderer.test.js index a769f33..92555bc 100644 --- a/tests/startup-renderer.test.js +++ b/tests/startup-renderer.test.js @@ -1,9 +1,68 @@ const test = require('node:test'); 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', () => { let calls = 0; configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }); 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); +});