const test = require('node:test'); const assert = require('node:assert/strict'); const { EventEmitter } = require('node:events'); const { execFileSync } = require('node:child_process'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { createStartupWindow, resolveStartupLanguage, createStartupQuery } = require('../lib/startup-renderer'); class TestBrowserWindow extends EventEmitter { constructor(options) { super(); this.webContents = new EventEmitter(); this.options = options; this.showCalls = 0; this.startupEvents = []; this.loadError = new Error('renderer load failed'); this.loadOptions = null; } once(eventName, listener) { this.startupEvents.push(`listen:${eventName}`); return super.once(eventName, listener); } show() { this.showCalls++; } loadFile(target, options) { this.startupEvents.push(`load:${target}`); this.loadOptions = options; return this.loadError ? Promise.reject(this.loadError) : Promise.resolve(); } } test('production startup never forces software compositing', () => { const projectRoot = path.join(__dirname, '..'); const pending = [path.join(projectRoot, 'main.js'), path.join(projectRoot, 'lib')]; const sourceFiles = []; while (pending.length) { const target = pending.pop(); const stat = fs.statSync(target); if (stat.isDirectory()) { for (const entry of fs.readdirSync(target)) pending.push(path.join(target, entry)); } else if (target.endsWith('.js')) { sourceFiles.push(target); } } for (const sourceFile of sourceFiles) { const source = fs.readFileSync(sourceFile, 'utf8'); assert.doesNotMatch(source, /disableHardwareAcceleration|disable-gpu(?:-compositing)?/u, path.relative(projectRoot, sourceFile)); } }); test('Windows compositor paints the full hidden surface with an RDP session environment', { skip: process.platform !== 'win32' }, () => { const projectRoot = path.join(__dirname, '..'); const probeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-rdp-compositor-')); const probePath = path.join(probeRoot, 'probe.cjs'); const preloadPath = path.join(probeRoot, 'preload.cjs'); const outputPath = path.join(probeRoot, 'result.json'); const userDataPath = path.join(probeRoot, 'user-data'); fs.writeFileSync(preloadPath, ` const { contextBridge } = require('electron'); const managedOnlineBackupProbeCalls = []; const managedOnlineBackupListResponses = [ { ok: true, entries: [ { id: 'AAAAAAAAAAAAAAAAAAAAAA', displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' }, { id: 'BBBBBBBBBBBBBBBBBBBBBB', displayKey: 'MHU2-ZYXW…9876', createdAt: '2026-08-22T10:00:00.000Z' } ] }, { ok: true, entries: [ { id: 'DDDDDDDDDDDDDDDDDDDDDD', displayKey: 'MHU2-DFGH…2468', createdAt: '2026-08-25T10:00:00.000Z' }, { id: 'AAAAAAAAAAAAAAAAAAAAAA', displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' } ] }, { ok: true, entries: [ { id: 'CCCCCCCCCCCCCCCCCCCCCC', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' }, { id: 'EEEEEEEEEEEEEEEEEEEEEE', displayKey: 'MHU2-ETYU…1357', createdAt: '2026-08-26T10:00:00.000Z' }, { id: 'DDDDDDDDDDDDDDDDDDDDDD', displayKey: 'MHU2-DFGH…2468', createdAt: '2026-08-25T10:00:00.000Z' } ] }, { ok: false, error: 'Online-Sicherungen konnten nicht geladen werden' } ]; let managedOnlineBackupListIndex = 0; let managedOnlineBackupCreateIndex = 0; let pendingManagedOnlineBackupList = null; let pendingManagedOnlineBackupCopy = null; let pendingManagedOnlineBackupDelete = null; contextBridge.exposeInMainWorld('api', { onUpdateAvailable() {}, onUpdateProgress() {}, onPrepareClose() {}, getConfig() { return new Promise(() => {}); }, listManagedOnlineBackups() { managedOnlineBackupListIndex++; managedOnlineBackupProbeCalls.push(['list', managedOnlineBackupListIndex]); if (managedOnlineBackupListIndex === 5) { return new Promise(resolve => { pendingManagedOnlineBackupList = resolve; }); } return Promise.resolve(managedOnlineBackupListResponses[managedOnlineBackupListIndex - 1]); }, releaseManagedOnlineBackupList() { const resolve = pendingManagedOnlineBackupList; pendingManagedOnlineBackupList = null; resolve({ ok: true, entries: [ { id: 'XXXXXXXXXXXXXXXXXXXXXX', displayKey: 'MHU2-XCVB…9753', createdAt: '2026-08-28T12:00:00.000Z' } ] }); }, createManagedOnlineBackup() { managedOnlineBackupCreateIndex++; managedOnlineBackupProbeCalls.push(['create', managedOnlineBackupCreateIndex]); if (managedOnlineBackupCreateIndex === 3) return Promise.resolve({ ok: false, error: 'Online-Sicherung konnte nicht erstellt werden' }); const entry = managedOnlineBackupCreateIndex === 1 ? { id: 'CCCCCCCCCCCCCCCCCCCCCC', displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' } : { id: 'FFFFFFFFFFFFFFFFFFFFFF', displayKey: 'MHU2-FGHJ…8642', createdAt: '2026-08-27T12:00:00.000Z' }; return Promise.resolve({ ok: true, entry }); }, copyManagedOnlineBackup(id) { managedOnlineBackupProbeCalls.push(['copy', id]); return new Promise(resolve => { pendingManagedOnlineBackupCopy = resolve; }); }, releaseManagedOnlineBackupCopy() { const resolve = pendingManagedOnlineBackupCopy; pendingManagedOnlineBackupCopy = null; resolve({ ok: true }); }, deleteManagedOnlineBackup(id) { managedOnlineBackupProbeCalls.push(['delete', id]); return new Promise(resolve => { pendingManagedOnlineBackupDelete = { id, resolve }; }); }, releaseManagedOnlineBackupDelete() { const pending = pendingManagedOnlineBackupDelete; pendingManagedOnlineBackupDelete = null; pending.resolve({ ok: true, removedId: pending.id, notFound: false }); }, getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; } }); `, 'utf8'); const onlineBackupBehaviorScript = `(async () => { const fixture = document.createElement('section'); fixture.innerHTML = '
'; document.body.append(fixture); const waitFor = async predicate => { for (let attempt = 0; attempt < 80; attempt++) { if (await predicate()) return true; await new Promise(resolve => setTimeout(resolve, 0)); } return false; }; let confirmation = null; let resolveConfirmation = null; showAppConfirm = options => { confirmation = options; return new Promise(resolve => { resolveConfirmation = resolve; }); }; flushPendingSettingsSaves = async () => {}; await loadManagedOnlineBackups(); const initialKeys = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent); const maliciousEntry = { id: 'DDDDDDDDDDDDDDDDDDDDDD', displayKey: 'prefix MHU2-' + 'X'.repeat(70) + ' suffix', createdAt: '2026-08-24T12:00:00.000Z' }; replaceManagedOnlineBackups([...managedOnlineBackups, maliciousEntry]); const secretRejected = !document.body.textContent.includes(maliciousEntry.displayKey) && managedOnlineBackups.every(entry => entry.displayKey !== maliciousEntry.displayKey); const originalRow = document.querySelector('.online-backup-managed-row'); originalRow.querySelector('.online-backup-delete-btn').click(); await waitFor(() => typeof resolveConfirmation === 'function'); renderManagedOnlineBackups(); const rowReplacedDuringConfirmation = !originalRow.isConnected; document.querySelector('.online-backup-copy-btn').click(); await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).some(call => call[0] === 'copy')); const copyPendingControlsDisabled = [...document.querySelector('.online-backup-managed-row').querySelectorAll('button')].every(button => button.disabled); resolveConfirmation(true); await new Promise(resolve => setTimeout(resolve, 0)); const deleteStartedBeforeCopyFinished = (await window.api.getManagedOnlineBackupProbeCalls()).some(call => call[0] === 'delete'); window.api.releaseManagedOnlineBackupCopy(); const deleteStartedAfterCopy = await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'delete').length === 1); const deletePendingControlsDisabled = deleteStartedAfterCopy && [...document.querySelector('.online-backup-managed-row').querySelectorAll('button')].every(button => button.disabled); if (deleteStartedAfterCopy) window.api.releaseManagedOnlineBackupDelete(); await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length === 2); const afterDelete = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent); const deletePostRefreshCount = (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length - 1; const listCallsBeforeCreate = (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length; await doOnlineBackupCreate(); const afterCreate = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent); const authoritativeStateFields = [...new Set(managedOnlineBackups.flatMap(entry => Object.keys(entry)))].sort(); const callsAfterCreate = await window.api.getManagedOnlineBackupProbeCalls(); const createPostRefreshCount = callsAfterCreate.filter(call => call[0] === 'list').length - listCallsBeforeCreate; const listCallsBeforeFailedCreate = callsAfterCreate.filter(call => call[0] === 'list').length; await doOnlineBackupCreate(); const failedCreatePostRefreshCount = (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length - listCallsBeforeFailedCreate; const refreshFailure = { rowCount: document.querySelectorAll('.online-backup-managed-row').length, stateCount: managedOnlineBackups.length, emptyStateVisible: Boolean(document.querySelector('.online-backup-managed-empty')), status: document.getElementById('onlineBackupStatus').textContent, statusState: document.getElementById('onlineBackupStatus').dataset.state }; const navigation = document.createElement('button'); navigation.dataset.settingsPage = 'backup'; navigation.addEventListener('click', () => { loadManagedOnlineBackups(); }); fixture.append(navigation); replaceManagedOnlineBackups([ { id: 'GGGGGGGGGGGGGGGGGGGGGG', displayKey: 'MHU2-GHJK…7531', createdAt: '2026-08-29T12:00:00.000Z' } ]); setOnlineBackupStatus('', ''); const failedCreate = doOnlineBackupCreate(); await waitFor(async () => (await window.api.getManagedOnlineBackupProbeCalls()).filter(call => call[0] === 'list').length === 5); await failedCreate; const statusBeforeStaleNavigationResponse = { text: document.getElementById('onlineBackupStatus').textContent, state: document.getElementById('onlineBackupStatus').dataset.state }; window.api.releaseManagedOnlineBackupList(); await new Promise(resolve => setTimeout(resolve, 0)); const staleNavigationResponse = { keys: [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent), status: document.getElementById('onlineBackupStatus').textContent, statusState: document.getElementById('onlineBackupStatus').dataset.state }; const calls = await window.api.getManagedOnlineBackupProbeCalls(); return { initialKeys, secretRejected, rowReplacedDuringConfirmation, copyPendingControlsDisabled, deleteStartedBeforeCopyFinished, deleteStartedAfterCopy, deletePendingControlsDisabled, deleteCallCount: calls.filter(call => call[0] === 'delete').length, afterDelete, deletePostRefreshCount, afterCreate, authoritativeStateFields, createPostRefreshCount, failedCreatePostRefreshCount, refreshFailure, statusBeforeStaleNavigationResponse, staleNavigationResponse, confirmation, calls, secretInBody: /MHU2-[A-Za-z0-9_-]{70}/.test(document.body.textContent) }; })()`; const onlineBackupLayoutScript = `(() => { const measure = language => { const copy = language === 'de' ? 'Schlüssel kopieren' : 'Copy key'; const remove = language === 'de' ? 'Online-Backup löschen' : 'Delete online backup'; document.documentElement.lang = language; document.body.innerHTML = '

Managed

ABCDEFGH…123422.08.2026 12:00
ZYXWVUTS…987621.08.2026 11:00
'; const panel = document.querySelector('.online-backup-panel'); const panelRect = panel.getBoundingClientRect(); const panelStyle = getComputedStyle(panel); const rows = [...document.querySelectorAll('.online-backup-managed-row')].map(row => { const key = row.querySelector('.online-backup-managed-key').getBoundingClientRect(); const created = row.querySelector('.online-backup-managed-created').getBoundingClientRect(); const actions = row.querySelector('.online-backup-managed-actions').getBoundingClientRect(); return { keyLeft: key.left, createdLeft: created.left, actionsRight: actions.right }; }); return { rows, contentRight: panelRect.right - parseFloat(panelStyle.paddingRight), createRight: document.querySelector('.online-backup-footer button').getBoundingClientRect().right }; }; return { german: measure('de'), english: measure('en') }; })()`; const onlineBackupNarrowLayoutScript = `(() => { document.body.innerHTML = '
ABCDEFGH…123422/08/2026, 12:00
'; const row = document.querySelector('.online-backup-managed-row').getBoundingClientRect(); const key = document.querySelector('.online-backup-managed-key').getBoundingClientRect(); const created = document.querySelector('.online-backup-managed-created').getBoundingClientRect(); const actions = document.querySelector('.online-backup-managed-actions').getBoundingClientRect(); const rowStyle = getComputedStyle(document.querySelector('.online-backup-managed-row')); const rowContentWidth = row.width - parseFloat(rowStyle.paddingLeft) - parseFloat(rowStyle.paddingRight) - parseFloat(rowStyle.borderLeftWidth) - parseFloat(rowStyle.borderRightWidth); const footer = document.querySelector('.online-backup-footer').getBoundingClientRect(); const create = document.querySelector('.online-backup-footer button').getBoundingClientRect(); return { horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1, rowOverflow: document.querySelector('.online-backup-managed-row').scrollWidth > document.querySelector('.online-backup-managed-row').clientWidth + 1, stacked: key.top < created.top && created.top < actions.top, actionsStretched: Math.abs(actions.width - rowContentWidth) <= 1, createStretched: Math.abs(create.width - footer.width) <= 1 }; })()`; const probeSource = ` const { app, BrowserWindow, screen } = require('electron'); const { execFileSync } = require('node:child_process'); const fs = require('node:fs'); const outputPath = process.env.MHU_RDP_COMPOSITOR_OUTPUT; function pixelAt(bitmap, width, x, y) { const offset = (y * width + x) * 4; return [bitmap[offset + 2], bitmap[offset + 1], bitmap[offset], bitmap[offset + 3]]; } app.whenReady().then(async () => { const display = screen.getPrimaryDisplay(); const requestedContentWidth = Math.min(2544, display.workAreaSize.width); const requestedContentHeight = Math.min(1353, display.workAreaSize.height); const window = new BrowserWindow({ show: false, width: requestedContentWidth, height: requestedContentHeight, useContentSize: true, backgroundColor: '#0f0f0f', webPreferences: { contextIsolation: true, nodeIntegration: false, preload: process.env.MHU_PRELOAD_PATH } }); const readyToShow = new Promise(resolve => window.once('ready-to-show', resolve)); const document = '
'; await window.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(document)); await readyToShow; await app.getGPUInfo('complete'); const image = await window.webContents.capturePage(); const size = image.getSize(); const bitmap = image.toBitmap(); const dom = await window.webContents.executeJavaScript('({ innerWidth, innerHeight, devicePixelRatio })'); const rendererPid = window.webContents.getOSProcessId(); const rendererCommandLine = execFileSync('powershell.exe', ['-NoProfile', '-Command', '(Get-CimInstance Win32_Process -Filter "ProcessId = ' + rendererPid + '").CommandLine'], { encoding: 'utf8' }).trim(); const middleY = Math.floor(size.height / 2); await window.loadFile(process.env.MHU_RENDERER_PATH, { query: { language: 'en', version: '2.1.31' } }); const liveSpeedChart = await window.webContents.executeJavaScript('({ baselinePresent: Boolean(document.querySelector(".upload-speed-baseline")), canvasWidth: document.getElementById("uploadSpeedCanvas")?.getBoundingClientRect().width || 0 })'); const onlineBackupBehavior = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupBehaviorScript)}); const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)}); window.setContentSize(760, Math.min(900, display.workAreaSize.height)); await new Promise(resolve => setTimeout(resolve, 50)); const onlineBackupNarrowLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupNarrowLayoutScript)}); fs.writeFileSync(outputPath, JSON.stringify({ size, dom, requestedContentWidth, requestedContentHeight, displayScaleFactor: display.scaleFactor, gpuFeatureStatus: app.getGPUFeatureStatus(), rendererCommandLine, leftEdge: pixelAt(bitmap, size.width, 0, middleY), rightEdge: pixelAt(bitmap, size.width, size.width - 1, middleY), liveSpeedChart, onlineBackupBehavior, onlineBackupLayout, onlineBackupNarrowLayout }), 'utf8'); window.destroy(); app.exit(0); }).catch(error => { fs.writeFileSync(outputPath, JSON.stringify({ error: error.stack || String(error) }), 'utf8'); app.exit(1); }); `; fs.writeFileSync(probePath, probeSource, 'utf8'); try { const electronPath = path.join(projectRoot, 'node_modules', 'electron', 'dist', 'electron.exe'); execFileSync(electronPath, [probePath, `--user-data-dir=${userDataPath}`], { cwd: projectRoot, env: { ...process.env, SESSIONNAME: 'RDP-Tcp#12', MHU_RDP_COMPOSITOR_OUTPUT: outputPath, MHU_RENDERER_PATH: path.join(projectRoot, 'renderer', 'index.html'), MHU_PRELOAD_PATH: preloadPath }, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30000, windowsHide: true }); const result = JSON.parse(fs.readFileSync(outputPath, 'utf8')); assert.equal(result.error, undefined); assert.doesNotMatch(result.rendererCommandLine, /--disable-gpu-compositing/u); assert.equal(result.size.width, Math.round(result.dom.innerWidth * result.dom.devicePixelRatio)); assert.equal(result.size.height, Math.round(result.dom.innerHeight * result.dom.devicePixelRatio)); assert.ok(result.requestedContentWidth > 0); assert.ok(result.requestedContentHeight > 0); if (Math.round(result.requestedContentWidth * result.displayScaleFactor) > 2048) { assert.ok(result.size.width > 2048); } assert.ok(result.leftEdge[1] > result.leftEdge[0] && result.leftEdge[1] > result.leftEdge[2]); assert.ok(result.rightEdge[0] > result.rightEdge[1] && result.rightEdge[2] > result.rightEdge[1]); assert.ok(result.liveSpeedChart.canvasWidth > 0); assert.equal(result.liveSpeedChart.baselinePresent, false); assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']); assert.equal(result.onlineBackupBehavior.secretRejected, true); assert.equal(result.onlineBackupBehavior.rowReplacedDuringConfirmation, true); assert.equal(result.onlineBackupBehavior.copyPendingControlsDisabled, true); assert.equal(result.onlineBackupBehavior.deleteStartedBeforeCopyFinished, false); assert.equal(result.onlineBackupBehavior.deleteStartedAfterCopy, true); assert.equal(result.onlineBackupBehavior.deletePendingControlsDisabled, true); assert.equal(result.onlineBackupBehavior.deleteCallCount, 1); assert.deepEqual(result.onlineBackupBehavior.afterDelete, ['MHU2-DFGH…2468', 'MHU2-ABCD…1234']); assert.equal(result.onlineBackupBehavior.deletePostRefreshCount, 1); assert.deepEqual(result.onlineBackupBehavior.afterCreate, ['MHU2-ETYU…1357', 'MHU2-DFGH…2468', 'MHU2-QWER…4321']); assert.deepEqual(result.onlineBackupBehavior.authoritativeStateFields, ['createdAt', 'displayKey', 'id']); assert.equal(result.onlineBackupBehavior.createPostRefreshCount, 1); assert.equal(result.onlineBackupBehavior.failedCreatePostRefreshCount, 1); assert.deepEqual(result.onlineBackupBehavior.refreshFailure, { rowCount: 0, stateCount: 0, emptyStateVisible: false, status: 'Online backups could not be loaded', statusState: 'error' }); assert.deepEqual(result.onlineBackupBehavior.statusBeforeStaleNavigationResponse, { text: 'Online backup could not be created', state: 'error' }); assert.deepEqual(result.onlineBackupBehavior.staleNavigationResponse, { keys: ['MHU2-GHJK…7531'], status: 'Online backup could not be created', statusState: 'error' }); assert.equal(result.onlineBackupBehavior.afterCreate.length, 3); assert.deepEqual(result.onlineBackupBehavior.confirmation, { title: 'Online-Backup löschen', message: 'Dieses verschlüsselte Online-Backup wird dauerhaft vom Server gelöscht.', confirmText: 'Löschen', danger: true }); assert.deepEqual(result.onlineBackupBehavior.calls, [ ['list', 1], ['copy', 'BBBBBBBBBBBBBBBBBBBBBB'], ['delete', 'BBBBBBBBBBBBBBBBBBBBBB'], ['list', 2], ['create', 1], ['list', 3], ['create', 2], ['list', 4], ['list', 5], ['create', 3] ]); assert.equal(result.onlineBackupBehavior.secretInBody, false); assert.ok(Math.abs(result.onlineBackupLayout.german.createRight - result.onlineBackupLayout.german.contentRight) <= 1); assert.ok(Math.abs(result.onlineBackupLayout.english.createRight - result.onlineBackupLayout.english.contentRight) <= 1); for (const layout of [result.onlineBackupLayout.german, result.onlineBackupLayout.english]) { assert.equal(layout.rows.length, 2); assert.ok(Math.abs(layout.rows[0].keyLeft - layout.rows[1].keyLeft) <= 1); assert.ok(Math.abs(layout.rows[0].createdLeft - layout.rows[1].createdLeft) <= 1); assert.ok(Math.abs(layout.rows[0].actionsRight - layout.rows[1].actionsRight) <= 1); } assert.ok(Math.abs(result.onlineBackupLayout.german.rows[0].keyLeft - result.onlineBackupLayout.english.rows[0].keyLeft) <= 1); assert.ok(Math.abs(result.onlineBackupLayout.german.rows[0].createdLeft - result.onlineBackupLayout.english.rows[0].createdLeft) <= 1); assert.ok(Math.abs(result.onlineBackupLayout.german.rows[0].actionsRight - result.onlineBackupLayout.english.rows[0].actionsRight) <= 1); assert.equal(result.onlineBackupNarrowLayout.horizontalOverflow, false); assert.equal(result.onlineBackupNarrowLayout.rowOverflow, false); assert.equal(result.onlineBackupNarrowLayout.stacked, true); assert.equal(result.onlineBackupNarrowLayout.actionsStretched, true); assert.equal(result.onlineBackupNarrowLayout.createStretched, true); } finally { fs.rmSync(probeRoot, { recursive: true, force: true }); } }); test('resolveStartupLanguage accepts only the supported persisted language', () => { assert.equal(resolveStartupLanguage({ globalSettings: { language: 'de' } }), 'de'); assert.equal(resolveStartupLanguage({ globalSettings: { language: 'en' } }), 'en'); assert.equal(resolveStartupLanguage({ globalSettings: { language: 'fr' } }), 'en'); assert.equal(resolveStartupLanguage(null), 'en'); }); test('startup query carries language and installed version into the first renderer frame', () => { assert.deepEqual(createStartupQuery({ globalSettings: { language: 'de' } }, '2.1.25'), { language: 'de', version: '2.1.25' }); assert.deepEqual(createStartupQuery(null, 'invalid'), { language: 'en', version: '' }); }); 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('main window uses the branded application icon', () => { const projectRoot = path.join(__dirname, '..'); const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8'); const createWindowStart = mainSource.indexOf('function createWindow()'); const createWindowEnd = mainSource.indexOf('\nfunction createTray()', createWindowStart); const createWindowSource = mainSource.slice(createWindowStart, createWindowEnd); assert.equal(fs.existsSync(path.join(projectRoot, 'assets', 'app_icon.ico')), true); assert.match(createWindowSource, /icon:\s*path\.join\(__dirname, ['"]assets['"], ['"]app_icon\.ico['"]\)/u); }); test('startup load registers visibility before navigation and shows only once', async () => { const startup = createStartupWindow(TestBrowserWindow, {}); startup.window.loadError = null; 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'); assert.equal(startup.window.showCalls, 0); startup.window.webContents.emit('did-finish-load'); startup.window.emit('ready-to-show'); startup.window.webContents.emit('did-finish-load'); await loading; assert.equal(startup.window.showCalls, 1); }); test('startup waits for native paint readiness when renderer loading finishes first', () => { const startup = createStartupWindow(TestBrowserWindow, {}); startup.window.webContents.emit('did-finish-load'); assert.equal(startup.window.showCalls, 0); startup.window.emit('ready-to-show'); 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); }); test('startup load forwards navigation options before the renderer becomes visible', async () => { const startup = createStartupWindow(TestBrowserWindow, {}); const options = { query: { language: 'de' } }; await startup.load('renderer/index.html', () => {}, options); assert.deepEqual(startup.window.loadOptions, options); }); test('desktop drag and drop is accepted before asynchronous renderer initialization', () => { const projectRoot = path.join(__dirname, '..'); const appSource = fs.readFileSync(path.join(projectRoot, 'renderer', 'app.js'), 'utf8'); const earlyBinding = appSource.lastIndexOf('\nsetupDragDrop();'); const initialization = appSource.lastIndexOf('\ninit().then('); assert.notEqual(earlyBinding, -1); assert.notEqual(initialization, -1); assert.ok(earlyBinding < initialization); assert.match(appSource, /dataTransfer\.dropEffect\s*=\s*['"]copy['"]/u); }); test('upload sidebar renders and updates the remaining upload size', () => { const projectRoot = path.join(__dirname, '..'); const html = fs.readFileSync(path.join(projectRoot, 'renderer', 'index.html'), 'utf8'); const appSource = fs.readFileSync(path.join(projectRoot, 'renderer', 'app.js'), 'utf8'); assert.match(html, /Verbleibende Größe[\s\S]*id="uploadTelemetryRemainingSize"[^>]*>0 B { const projectRoot = path.join(__dirname, '..'); const html = fs.readFileSync(path.join(projectRoot, 'renderer', 'index.html'), 'utf8'); const appSource = fs.readFileSync(path.join(projectRoot, 'renderer', 'app.js'), 'utf8'); const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8'); const css = fs.readFileSync(path.join(projectRoot, 'renderer', 'styles.css'), 'utf8'); const updateButton = html.match(/