/** * UI smoke test - launches the real app and checks DOM elements via webContents. * Run with: node tests/ui-smoke.js * (This spawns Electron as a child process) */ if (!process.env.RUN_UI_SMOKE) { const { test } = require('node:test'); test('ui smoke skipped unless RUN_UI_SMOKE=1', () => {}); return; } const { execFileSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const os = require('os'); const uiRunId = `${process.pid}-${Date.now()}`; const visualScreenshotDir = process.env.MHU_UI_SCREENSHOT_DIR ? path.resolve(process.env.MHU_UI_SCREENSHOT_DIR) : ''; const injectPath = path.join(__dirname, `_ui-inject.${uiRunId}.tmp.js`); const userDataPath = path.join(os.tmpdir(), `mhu-ui-smoke-${uiRunId}`); if (visualScreenshotDir) fs.mkdirSync(visualScreenshotDir, { recursive: true }); // Create a temp script that the real Electron app will execute via --eval const testScript = ` const { app, BrowserWindow, ipcMain, dialog } = require('electron'); const fs = require('fs'); const net = require('net'); const path = require('path'); const ConfigStore = require(path.join(process.cwd(), 'lib', 'config-store')); const updaterModule = require(path.join(process.cwd(), 'lib', 'updater')); let preparedUpdateMockCalls = 0; let launchedUpdateMockCalls = 0; let updateCheckMockCalls = 0; updaterModule.checkForUpdate = async () => { updateCheckMockCalls++; return updateCheckMockCalls === 1 ? { available: true, remoteVersion: '9.9.8' } : { available: false }; }; updaterModule.prepareUpdate = async (onProgress) => { preparedUpdateMockCalls++; if (onProgress) onProgress({ stage: 'prepared', percent: 100 }); return { installerPath: path.join(app.getPath('temp'), 'mhu-ui-update-' + preparedUpdateMockCalls + '.exe') }; }; updaterModule.launchPreparedUpdate = () => { launchedUpdateMockCalls++; return true; }; const initialIpcHandlers = new Map(); const registerIpcHandler = ipcMain.handle.bind(ipcMain); let initialConfigReadDelayed = false; ipcMain.handle = (channel, listener) => { const registeredListener = channel === 'get-config' ? async (...args) => { const result = await listener(...args); if (!initialConfigReadDelayed) { initialConfigReadDelayed = true; await new Promise(resolve => setTimeout(resolve, 4000)); } return result; } : listener; if (!initialIpcHandlers.has(channel)) initialIpcHandlers.set(channel, registeredListener); return registerIpcHandler(channel, registeredListener); }; function restoreInitialIpcHandler(channel) { ipcMain.removeHandler(channel); const listener = initialIpcHandlers.get(channel); if (listener) registerIpcHandler(channel, listener); } const originalAtomicWrite = ConfigStore.prototype._atomicWrite; const originalHistoryAtomicWrite = ConfigStore.prototype._writeHistoryFileAtomic; let activeConfigStore = null; let blockedWriteMarker = ''; let blockedWriteStarted = false; let releaseBlockedWrite = null; let blockedHistoryWriteMarker = ''; let blockedHistoryWriteStarted = false; let releaseBlockedHistoryWrite = null; ConfigStore.prototype._atomicWrite = function (data) { activeConfigStore = this; if (blockedWriteMarker && !blockedWriteStarted && String(data).includes(blockedWriteMarker)) { blockedWriteStarted = true; return new Promise((resolve, reject) => { releaseBlockedWrite = () => originalAtomicWrite.call(this, data).then(resolve, reject); }); } return originalAtomicWrite.call(this, data); }; ConfigStore.prototype._writeHistoryFileAtomic = function (history) { activeConfigStore = this; if (blockedHistoryWriteMarker && !blockedHistoryWriteStarted && JSON.stringify(history).includes(blockedHistoryWriteMarker)) { blockedHistoryWriteStarted = true; return new Promise((resolve, reject) => { releaseBlockedHistoryWrite = () => originalHistoryAtomicWrite.call(this, history).then(resolve, reject); }); } return originalHistoryAtomicWrite.call(this, history); }; // Monkey-patch: after the real window loads, run tests const origReady = app.whenReady; async function runAfterDelay(win, delayMs) { await new Promise(r => setTimeout(r, delayMs)); return win; } async function waitUntil(read, timeoutMs = 3000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const value = await read(); if (value) return value; await new Promise(resolve => setTimeout(resolve, 10)); } return null; } // Wait for app to be ready, then wait for the real window to load setTimeout(async () => { const windows = BrowserWindow.getAllWindows(); if (windows.length === 0) { console.log('ERROR: No windows found'); process.exit(1); } const win = windows[0]; const wc = win.webContents; const originalBounds = win.getBounds(); const visualScreenshotDir = ${JSON.stringify(visualScreenshotDir)}; async function captureVisual(name) { if (!visualScreenshotDir) return; await new Promise(resolve => setTimeout(resolve, 150)); const screenshot = await wc.capturePage(); fs.writeFileSync(path.join(visualScreenshotDir, name), screenshot.toPNG()); } // Wait for renderer init await new Promise(r => setTimeout(r, 2000)); let passed = 0; let failed = 0; const results = []; let realAppQuit = null; let relaunchCalls = 0; function check(name, condition) { if (condition) { passed++; results.push(' PASS: ' + name); } else { failed++; results.push(' FAIL: ' + name); } } try { const startupUpdateState = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [_knownUpdateInfo?.remoteVersion, button?.hidden, getComputedStyle(button).display, document.getElementById("updateBanner")?.style.display].join("|"); })()'); check('Startup update survives pending renderer initialization', startupUpdateState === '9.9.8|false|flex|flex'); await wc.executeJavaScript('_knownUpdateInfo = null; closeUpdateDialog(); _syncHeaderUpdateState();'); await wc.executeJavaScript('queueJobs = []; selectedFiles = []; selectedJobIds.clear(); rebuildJobIndex(); setUploadSidebarFilter("all"); updateUploadView(); renderQueueTable(); updateStatusBar();'); console.log('\\n=== Upload View ==='); const tabCount = await wc.executeJavaScript('document.querySelectorAll(".tab").length'); check('4 tabs exist', tabCount === 4); const appHeaderExists = await wc.executeJavaScript('Boolean(document.querySelector(".app-header"))'); check('App shell exposes the primary header', appHeaderExists); const appBrandText = await wc.executeJavaScript('document.querySelector(".app-brand-name")?.textContent?.trim()'); check('App header shows the Multi-Hoster Upload brand', appBrandText === 'MULTI-HOSTER UPLOAD'); const topbarIconCount = await wc.executeJavaScript('document.querySelectorAll(".app-header .tab .top-nav-icon").length'); check('App header exposes exactly four topbar icons', topbarIconCount === 4); const headerUpdateButtonExists = await wc.executeJavaScript('Boolean(document.getElementById("headerUpdateBtn"))'); check('App header exposes the update action', headerUpdateButtonExists); const initialHeaderUpdateVisibility = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [button?.hidden, getComputedStyle(button).display].join("|"); })()'); check('Header update action stays hidden until an update is available', initialHeaderUpdateVisibility === 'true|none'); const initialUpdateLabel = await wc.executeJavaScript('document.querySelector("#headerUpdateBtn .header-update-label")?.textContent?.trim()'); check('App header uses the compact update label', initialUpdateLabel === 'Update'); const bodyBackground = await wc.executeJavaScript('getComputedStyle(document.body).backgroundColor'); check('App shell uses the dark reference canvas', bodyBackground === 'rgb(15, 15, 15)'); const shellDensity = await wc.executeJavaScript('(() => ({ header: document.querySelector(".app-header")?.getBoundingClientRect().height, status: document.getElementById("statusbar")?.getBoundingClientRect().height, sidebar: document.querySelector("#upload-view > .view-sidebar")?.getBoundingClientRect().width }))()'); check('App shell keeps a compact desktop density', shellDensity.header <= 50 && shellDensity.status <= 30 && shellDensity.sidebar <= 230); const tabLabels = await wc.executeJavaScript('[...document.querySelectorAll(".tab")].map(el => el.textContent.trim()).join("|")'); check('Current tab labels present', tabLabels === 'Upload|Accounts|Einstellungen|Verlauf'); const tabSemantics = await wc.executeJavaScript('document.querySelector(".tab-bar")?.getAttribute("role") + "|" + document.querySelector(".tab.active")?.getAttribute("aria-selected")'); check('Tab navigation exposes active state', tabSemantics === 'tablist|true'); const activeTab = await wc.executeJavaScript('document.querySelector(".tab.active")?.textContent?.trim()'); check('Upload tab active by default', activeTab === 'Upload'); const tabStops = await wc.executeJavaScript('[...document.querySelectorAll(".tab")].map(el => el.tabIndex).join("|")'); check('Tab navigation exposes one keyboard stop', tabStops === '0|-1|-1|-1'); const dropVisible = await wc.executeJavaScript('document.getElementById("dropZone")?.style.display !== "none"'); check('Drop zone visible (no files)', dropVisible); const queueHidden = await wc.executeJavaScript('document.getElementById("queueShell")?.style.display'); check('Queue hidden (no files)', queueHidden === 'none'); const startDisabled = await wc.executeJavaScript('document.getElementById("startUploadBtn")?.disabled'); check('Start button disabled initially', startDisabled === true); const sbState = await wc.executeJavaScript('document.getElementById("sbState")?.textContent'); check('Statusbar: Bereit', sbState === 'Bereit'); const version = await wc.executeJavaScript('document.getElementById("versionLabel")?.textContent'); check('Version label present', version && version.startsWith('v')); const localizedQueueHeaders = await wc.executeJavaScript('[...document.querySelectorAll("#queueTable thead th")].map(el => el.childNodes[0]?.textContent.trim()).join("|")'); check('Upload table labels are consistently German', localizedQueueHeaders === 'Dateiname|Hochgeladen / Größe|Hoster|Status|Zeit|Rest|Geschwindigkeit|Fortschritt'); const localizedRecentTabs = await wc.executeJavaScript('[...document.querySelectorAll(".recent-tab")].map(el => el.textContent.trim()).join("|")'); check('Recent panel labels are consistently German', localizedRecentTabs === 'Dateien|Statistik'); const localizedStatusbar = await wc.executeJavaScript('["sbConnections", "sbQueueCount", "sbRemainingCount", "sbInProgressCount", "sbDoneCount", "sbErrorCount"].map(id => document.getElementById(id)?.textContent).join("|")'); check('Statusbar labels are consistently German', localizedStatusbar === 'Verbindungen 0|Gesamt 0|Verbleibend 0|Läuft 0|Fertig 0|Fehler 0'); const toolbarLabels = await wc.executeJavaScript('[...document.querySelectorAll("#queueCommandBar .toolbar-btn")].map(el => el.getAttribute("aria-label")).join("|")'); check('Upload toolbar actions have German accessible names', toolbarLabels === 'Alle Uploads starten|Ausgewählte Uploads starten|Ausgewählte Datei erneut hochladen|Ausgewählten Upload abbrechen|Aktive Uploads beenden und stoppen|Alle Uploads abbrechen|Ganz nach oben|Nach oben|Nach unten|Ganz nach unten'); const uploadWorkspaceLayout = await wc.executeJavaScript('(() => { const view = document.getElementById("upload-view"); const sidebar = view?.querySelector(":scope > .view-sidebar"); const main = view?.querySelector(":scope > .view-main"); if (!sidebar || !main) return false; const sidebarRect = sidebar.getBoundingClientRect(); const mainRect = main.getBoundingClientRect(); return sidebarRect.width > 0 && mainRect.width > 0 && sidebarRect.right <= mainRect.left; })()'); check('Upload view separates sidebar and main workspace', uploadWorkspaceLayout === true); const uploadSidebarInformation = await wc.executeJavaScript('(() => { const sidebar = document.querySelector("#upload-view > .view-sidebar")?.getBoundingClientRect(); const section = document.querySelector("#upload-view .view-sidebar-section")?.getBoundingClientRect(); return Boolean(sidebar && section && section.top >= sidebar.top + sidebar.height * 0.55 && document.getElementById("uploadSidebarAccountsCount")); })()'); check('Upload sidebar keeps availability information in its lower area', uploadSidebarInformation === true); const uploadFrameFit = await wc.executeJavaScript('(() => { const view = document.getElementById("upload-view")?.getBoundingClientRect(); const status = document.getElementById("statusbar")?.getBoundingClientRect(); return Boolean(view && status && status.height > 0 && view.bottom <= status.top + 1 && status.bottom <= window.innerHeight + 1); })()'); check('Upload view and statusbar fit inside the viewport', uploadFrameFit === true); await captureVisual('01-upload.png'); const uploadFilterState = await wc.executeJavaScript(\`(() => { const makeJob = (id, fileName, status) => ({ id, file: 'C:/ui/' + fileName, fileName, hoster: 'byse.sx', status, bytesUploaded: 0, bytesTotal: 1024, speedKbs: 0, elapsed: 0, remaining: 0, progress: status === 'done' ? 1 : 0 }); const waiting = Array.from({ length: 201 }, (_, index) => makeJob('ui-wait-' + index, 'wait-' + String(index).padStart(3, '0') + '.bin', 'queued')); queueJobs = [makeJob('ui-active-z', 'active-z.bin', 'uploading'), ...waiting, makeJob('ui-error', 'error.bin', 'error'), makeJob('ui-done', 'done.bin', 'done'), makeJob('ui-active-a', 'active-a.bin', 'retrying')]; queueSortState.key = 'filename'; queueSortState.direction = 'asc'; selectedJobIds.clear(); selectedJobIds.add('ui-active-z'); selectedJobIds.add('ui-error'); const sourceOrder = queueJobs.map(job => job.id).join(','); document.getElementById('queueContainer').scrollTop = 0; renderQueueTable(); const allState = { working: _sortedJobsCache.length, rendered: document.querySelectorAll('#queueBody .queue-row').length, spacer: Boolean(document.querySelector('#queueBody .virtual-spacer')), height: document.querySelector('#queueBody .queue-row')?.style.height }; const inspect = (value) => { document.querySelector('[data-upload-sidebar-target="' + value + '"]').click(); return { working: _sortedJobsCache.length, ids: [...document.querySelectorAll('#queueBody .queue-row')].map(row => row.dataset.jobId), names: [...document.querySelectorAll('#queueBody .queue-row .col-filename')].map(cell => cell.textContent), spacer: Boolean(document.querySelector('#queueBody .virtual-spacer')), selected: [...selectedJobIds], pressed: [...document.querySelectorAll('[data-upload-sidebar-target]')].filter(button => button.getAttribute('aria-pressed') === 'true').map(button => button.dataset.uploadSidebarTarget), active: [...document.querySelectorAll('[data-upload-sidebar-target].active')].map(button => button.dataset.uploadSidebarTarget) }; }; const active = inspect('active'); const waitingState = inspect('waiting'); const done = inspect('done'); const error = inspect('error'); const restored = inspect('all'); const sourceUnchanged = sourceOrder === queueJobs.map(job => job.id).join(','); queueJobs = [makeJob('ui-only-waiting', 'only-waiting.bin', 'queued')]; const emptyError = inspect('error'); const emptySafe = queueJobs.length === 1 && emptyError.working === 0 && emptyError.ids.length === 0; queueJobs = []; document.querySelector('[data-upload-sidebar-target="all"]').click(); updateStatusBar(); return { allState, active, waitingState, done, error, restored, sourceUnchanged, emptySafe }; })()\`); check('Upload sidebar filters the visible queue without mutating source order', uploadFilterState.active.working === 2 && uploadFilterState.active.names.join('|') === 'active-a.bin|active-z.bin' && uploadFilterState.waitingState.working === 201 && uploadFilterState.done.ids.join('|') === 'ui-done' && uploadFilterState.error.ids.join('|') === 'ui-error' && uploadFilterState.restored.working === 205 && uploadFilterState.sourceUnchanged); check('Upload sidebar retains 28px virtual rows for large filtered queues', uploadFilterState.allState.working === 205 && uploadFilterState.allState.rendered > 0 && uploadFilterState.allState.rendered < 205 && uploadFilterState.allState.spacer && uploadFilterState.allState.height === '28px' && uploadFilterState.waitingState.spacer); check('Upload sidebar exposes one pressed filter and handles empty matches', uploadFilterState.active.pressed.join('|') === 'active' && uploadFilterState.active.active.join('|') === 'active' && uploadFilterState.error.pressed.join('|') === 'error' && uploadFilterState.error.active.join('|') === 'error' && uploadFilterState.emptySafe); check('Upload sidebar drops hidden selections when changing filters', uploadFilterState.active.selected.join('|') === 'ui-active-z'); const uploadSelectionScope = await wc.executeJavaScript(\`(() => { const makeJob = (id, status) => ({ id, file: 'C:/ui/' + id + '.bin', fileName: id + '.bin', hoster: 'byse.sx', status, bytesUploaded: 0, bytesTotal: 1024, speedKbs: 0, elapsed: 0, remaining: 0, progress: status === 'done' ? 1 : 0 }); const activeA = makeJob('scope-active-a', 'uploading'); const activeZ = makeJob('scope-active-z', 'retrying'); const error = makeJob('scope-error', 'error'); const done = makeJob('scope-done', 'done'); queueJobs = [activeZ, error, done, activeA]; rebuildJobIndex(); selectedJobIds.clear(); selectedRecentIds.clear(); setUploadSidebarFilter('active'); document.getElementById('queueContainer').dispatchEvent(new KeyboardEvent('keydown', { key: 'a', ctrlKey: true, bubbles: true })); const ctrlASelected = [...selectedJobIds].sort(); selectedJobIds.clear(); selectedJobIds.add(activeA.id); activeA.status = 'done'; renderQueueTable(); const statusChangeSelected = [...selectedJobIds]; const statusChangeVisible = _sortedJobsCache.map(job => job.id); queueJobs = [activeZ, error, done, activeA]; activeA.status = 'uploading'; rebuildJobIndex(); setUploadSidebarFilter('active'); selectedJobIds.clear(); selectedJobIds.add(error.id); updateQueueActionButtons(); const hiddenAction = { selected: [...selectedJobIds], retryDisabled: document.getElementById('reuploadSelectedBtn').disabled, moveDisabled: document.getElementById('moveTopBtn').disabled }; queueJobs = []; selectedJobIds.clear(); rebuildJobIndex(); setUploadSidebarFilter('all'); updateStatusBar(); return { ctrlASelected, statusChangeSelected, statusChangeVisible, hiddenAction }; })()\`); check('Ctrl+A selects only jobs visible in the upload filter', uploadSelectionScope.ctrlASelected.join('|') === 'scope-active-a|scope-active-z'); check('Status changes drop selections that leave the upload filter', uploadSelectionScope.statusChangeSelected.length === 0 && uploadSelectionScope.statusChangeVisible.join('|') === 'scope-active-z'); check('Selected upload actions ignore hidden stale selections', uploadSelectionScope.hiddenAction.selected.length === 0 && uploadSelectionScope.hiddenAction.retryDisabled && uploadSelectionScope.hiddenAction.moveDisabled); const keyboardTab = await wc.executeJavaScript('document.getElementById("upload-tab").focus(); document.getElementById("upload-tab").dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true })); document.querySelector(".tab.active")?.textContent?.trim() + "|" + document.activeElement?.id'); check('Arrow keys move and activate main tabs', keyboardTab === 'Accounts|accounts-tab'); const ctxHidden = await wc.executeJavaScript('document.getElementById("contextMenu")?.style.display'); check('Context menu hidden', ctxHidden === 'none'); console.log('\\n=== Accounts View ==='); await wc.executeJavaScript('document.querySelector(".tab[data-view=\\'accounts\\']").click()'); await new Promise(r => setTimeout(r, 300)); const accountsActive = await wc.executeJavaScript('document.getElementById("accounts-view")?.classList.contains("active")'); check('Accounts tab active', accountsActive); const accountsWorkspaceLayout = await wc.executeJavaScript('(() => { const view = document.getElementById("accounts-view"); const sidebar = view?.querySelector(":scope > .view-sidebar"); const main = view?.querySelector(":scope > .view-main"); if (!sidebar || !main) return false; const sidebarRect = sidebar.getBoundingClientRect(); const mainRect = main.getBoundingClientRect(); return sidebarRect.width > 0 && mainRect.width > 0 && sidebarRect.right <= mainRect.left; })()'); check('Accounts view separates sidebar and main workspace', accountsWorkspaceLayout === true); const accountSidebarInformation = await wc.executeJavaScript('(() => { const sidebar = document.querySelector("#accounts-view > .view-sidebar")?.getBoundingClientRect(); const section = document.querySelector("#accounts-view .view-sidebar-hoster-section")?.getBoundingClientRect(); return Boolean(sidebar && section && section.top >= sidebar.top + sidebar.height * 0.55); })()'); check('Account sidebar keeps hoster information in its lower area', accountSidebarInformation === true); const accountsFrameFit = await wc.executeJavaScript('(() => { const view = document.getElementById("accounts-view")?.getBoundingClientRect(); const status = document.getElementById("statusbar")?.getBoundingClientRect(); return Boolean(view && status && status.height > 0 && view.bottom <= status.top + 1 && status.bottom <= window.innerHeight + 1); })()'); check('Accounts view and statusbar fit inside the viewport', accountsFrameFit === true); await captureVisual('02-accounts.png'); const accountListValid = await wc.executeJavaScript('Boolean(document.querySelector("#accountsList .accounts-empty") || document.querySelectorAll("#accountsList .account-hoster-group").length)'); check('Account manager list structure rendered', accountListValid); const addAccountEnabled = await wc.executeJavaScript('document.getElementById("addAccountBtn")?.disabled === false'); check('Add account button enabled', addAccountEnabled); const emptyAccountAction = await wc.executeJavaScript('document.querySelector("[data-account-empty-add]")?.textContent?.trim() || document.getElementById("addAccountBtn")?.textContent?.trim()'); check('Account list offers direct add action', emptyAccountAction === 'Ersten Account hinzufügen' || emptyAccountAction === 'Account hinzufügen'); const accountEmptyStateActionCount = await wc.executeJavaScript('document.querySelectorAll("#accountsList [data-account-empty-add]").length'); check('Account empty state avoids a duplicate primary action', accountEmptyStateActionCount === 0); await wc.executeJavaScript('(() => { const trigger = document.querySelector("[data-account-empty-add]") || document.getElementById("addAccountBtn"); trigger?.focus(); trigger?.click(); })()'); await new Promise(r => setTimeout(r, 200)); const accountModalVisible = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display'); check('Account modal opens', accountModalVisible === 'flex'); const accountModalTitle = await wc.executeJavaScript('document.getElementById("accountModalTitle")?.textContent'); check('Account modal is in add mode', accountModalTitle === 'Account hinzufügen'); const accountModalSemantics = await wc.executeJavaScript('document.querySelector("#accountModal .modal-card")?.getAttribute("role") + "|" + document.querySelector("#accountModal .modal-card")?.getAttribute("aria-modal")'); check('Account modal exposes dialog semantics', accountModalSemantics === 'dialog|true'); const accountFormLabels = await wc.executeJavaScript('["accountHosterSelect", "accField_label", "accField_username", "accField_password"].every(id => document.getElementById(id)?.labels?.length === 1)'); check('Account form controls have linked labels', accountFormLabels); const accountStatusLive = await wc.executeJavaScript('document.getElementById("accountModalStatus")?.getAttribute("aria-live")'); check('Account validation status is announced', accountStatusLive === 'polite'); const initialAccountFocus = await wc.executeJavaScript('document.activeElement?.id'); check('Account modal focuses first control', initialAccountFocus === 'accountHosterSelect'); const trappedAccountFocus = await wc.executeJavaScript('document.getElementById("saveAccountBtn").focus(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true })); document.activeElement?.id'); check('Account modal keeps keyboard focus inside', trappedAccountFocus === 'closeAccountModalBtn'); const authOptionCount = await wc.executeJavaScript('document.querySelectorAll("#accountHosterSelect option").length'); check('7 hoster authentication options exist', authOptionCount === 7); const hosterCount = await wc.executeJavaScript('[...new Set([...document.querySelectorAll("#accountHosterSelect option")].map(el => el.value.split(":")[0]))].length'); check('5 hosters exist', hosterCount === 5); const accountSubmitLabel = await wc.executeJavaScript('document.getElementById("saveAccountBtn")?.textContent'); check('Account submit label is Prüfen und speichern', accountSubmitLabel === 'Prüfen und speichern'); const credentialInputs = await wc.executeJavaScript('document.querySelectorAll("#accountCredsFields .key-input").length'); check('Credential inputs rendered', credentialInputs === 2); const passwordToggleState = await wc.executeJavaScript('document.querySelector("#accountCredsFields .toggle-vis").click(); document.querySelector("#accountCredsFields .toggle-vis").getAttribute("aria-label") + "|" + document.querySelector("#accountCredsFields .toggle-vis").getAttribute("aria-pressed")'); check('Password visibility action exposes its state', passwordToggleState === 'Passwort verbergen|true'); await wc.executeJavaScript('document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }))'); const accountModalHidden = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display'); check('Escape closes account modal', accountModalHidden === 'none'); const restoredAccountFocus = await wc.executeJavaScript('document.activeElement?.hasAttribute("data-account-empty-add") || document.activeElement?.id === "addAccountBtn"'); check('Account modal restores trigger focus', restoredAccountFocus === true); const fallbackAccountFocus = await wc.executeJavaScript('(() => { const trigger = document.querySelector("[data-account-empty-add]") || document.getElementById("addAccountBtn"); trigger.focus(); trigger.click(); document.querySelector("[data-account-empty-add]")?.remove(); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); return document.activeElement?.id; })()'); check('Account modal restores stable focus after list rerender', fallbackAccountFocus === 'addAccountBtn'); const mixedGroupStatus = await wc.executeJavaScript(\`(() => { config.hosters['byse.sx'] = [ { id: 'ui-status-ok-1', enabled: true, authType: 'api', apiKey: 'key-one' }, { id: 'ui-status-ok-2', enabled: true, authType: 'api', apiKey: 'key-two' }, { id: 'ui-status-error', enabled: true, authType: 'api', apiKey: 'key-three' } ]; accountStatuses['ui-status-ok-1'] = { status: 'ok', message: 'Bereit' }; accountStatuses['ui-status-ok-2'] = { status: 'ok', message: 'Bereit' }; accountStatuses['ui-status-error'] = { status: 'error', message: 'Deaktiviert durch Administrator' }; renderAccounts(); const dot = document.querySelector('.account-hoster-group[data-hoster-group="byse.sx"] .account-hoster-group-header .account-status-dot'); return [dot?.className, getComputedStyle(dot).backgroundColor].join('|'); })()\`); check('Mixed account results use orange group status', mixedGroupStatus === 'account-status-dot status-warn|rgb(240, 195, 108)'); const allErrorGroupStatus = await wc.executeJavaScript(\`(() => { accountStatuses['ui-status-ok-1'] = { status: 'error', message: 'Fehler' }; accountStatuses['ui-status-ok-2'] = { status: 'error', message: 'Fehler' }; renderAccounts(); return document.querySelector('.account-hoster-group[data-hoster-group="byse.sx"] .account-hoster-group-header .account-status-dot')?.className; })()\`); check('All-error account group uses red status', allErrorGroupStatus === 'account-status-dot status-error'); const otpCardState = await wc.executeJavaScript(\`(() => { config.hosters['doodstream.com'] = [{ id: 'ui-status-otp', enabled: true, authType: 'login', username: 'otp@example.com', password: 'password' }]; accountStatuses['ui-status-otp'] = { status: 'otp_required', message: 'OTP wurde an deine E-Mail gesendet.' }; renderAccounts(); const card = [...document.querySelectorAll('.account-card')].find(el => el.dataset.accountId === 'ui-status-otp'); return [ card?.querySelector('[data-account-otp-input]')?.getAttribute('placeholder'), card?.querySelector('[data-account-otp-submit]')?.textContent?.trim(), card?.querySelector('.account-status')?.textContent?.trim() ].join('|'); })()\`); check('OTP-required account exposes inline code input and save action', otpCardState === 'Code aus E-Mail|Prüfen und speichern|OTP erforderlich'); const otpSubmitState = await wc.executeJavaScript(\`(() => { const card = [...document.querySelectorAll('.account-card')].find(el => el.dataset.accountId === 'ui-status-otp'); card.querySelector('[data-account-otp-input]').value = '123456'; card.querySelector('[data-account-otp-submit]').click(); return [accountStatuses['ui-status-otp']?.status, String(card.querySelector('[data-account-otp-submit]')?.disabled)].join('|'); })()\`); check('Inline OTP submission starts validation', otpSubmitState === 'checking|true'); const accountFilterState = await wc.executeJavaScript(\`(() => { HOSTERS.forEach(name => { config.hosters[name] = []; }); config.hosters['doodstream.com'] = [ { id: 'ui-filter-no-creds', enabled: true, authType: 'login', username: '', password: '' } ]; config.hosters['byse.sx'] = [ { id: 'ui-filter-ready', enabled: true, authType: 'api', apiKey: 'ready-key' }, { id: 'ui-filter-disabled', enabled: false, authType: 'api', apiKey: 'disabled-key' }, { id: 'ui-filter-error', enabled: true, authType: 'api', apiKey: 'error-key' } ]; accountStatuses = { 'ui-filter-no-creds': { status: 'ok', message: 'Bereit' }, 'ui-filter-ready': { status: 'ok', message: 'Bereit' }, 'ui-filter-disabled': { status: 'ok', message: 'Bereit' }, 'ui-filter-error': { status: 'error', message: 'Fehler' } }; renderAccounts(); const inspect = (value) => { document.querySelector('[data-accounts-sidebar-filter="' + value + '"]').click(); return { cards: [...document.querySelectorAll('#accountsList .account-card')].filter(card => !card.hidden).map(card => card.dataset.accountId).sort(), groups: [...document.querySelectorAll('#accountsList .account-hoster-group')].filter(group => !group.hidden).map(group => group.dataset.hosterGroup).sort(), pressed: [...document.querySelectorAll('[data-accounts-sidebar-filter]')].filter(button => button.getAttribute('aria-pressed') === 'true').map(button => button.dataset.accountsSidebarFilter), active: [...document.querySelectorAll('[data-accounts-sidebar-filter].active')].map(button => button.dataset.accountsSidebarFilter) }; }; const ready = inspect('ready'); const warning = inspect('warning'); const error = inspect('error'); const all = inspect('all'); const counters = ['accountsSidebarAllCount', 'accountsSidebarReadyCount', 'accountsSidebarWarningCount', 'accountsSidebarErrorCount'].map(id => document.getElementById(id)?.textContent); return { ready, warning, error, all, counters }; })()\`); check('Account sidebar filters cards and hides hoster groups without matches', accountFilterState.ready.cards.join('|') === 'ui-filter-ready' && accountFilterState.ready.groups.join('|') === 'byse.sx' && accountFilterState.warning.cards.join('|') === 'ui-filter-disabled|ui-filter-no-creds' && accountFilterState.warning.groups.join('|') === 'byse.sx|doodstream.com' && accountFilterState.error.cards.join('|') === 'ui-filter-error' && accountFilterState.error.groups.join('|') === 'byse.sx' && accountFilterState.all.cards.length === 4); check('Account sidebar classifies disabled and credential-less accounts as action needed', accountFilterState.counters.join('|') === '4|1|2|1'); check('Account sidebar exposes exactly one pressed filter', accountFilterState.error.pressed.join('|') === 'error' && accountFilterState.error.active.join('|') === 'error' && accountFilterState.all.pressed.join('|') === 'all' && accountFilterState.all.active.join('|') === 'all'); console.log('\\n=== Settings View ==='); await wc.executeJavaScript('document.querySelector(".tab[data-view=\\'settings\\']").click()'); await new Promise(r => setTimeout(r, 300)); const settingsActive = await wc.executeJavaScript('document.getElementById("settings-view")?.classList.contains("active")'); check('Settings tab active', settingsActive); let updateCheckCallCount = 0; const updateCheckResolvers = []; ipcMain.removeHandler('app:check-updates'); ipcMain.handle('app:check-updates', () => { updateCheckCallCount++; return new Promise(resolve => updateCheckResolvers.push(resolve)); }); await wc.executeJavaScript(\`(() => { _knownUpdateInfo = null; _updateCheckBusy = false; closeUpdateDialog(); _syncHeaderUpdateState(); document.getElementById('manualUpdateCheckBtn').click(); _handleMenuAction('check-updates'); document.getElementById('headerUpdateBtn').click(); })()\`); await new Promise(resolve => setTimeout(resolve, 100)); const coordinatedUpdateBusy = await wc.executeJavaScript('(() => { const manual = document.getElementById("manualUpdateCheckBtn"); const header = document.getElementById("headerUpdateBtn"); return [manual?.disabled, manual?.getAttribute("aria-busy"), manual?.textContent?.trim(), header?.disabled, header?.getAttribute("aria-busy"), header?.hidden].join("|"); })()'); check('All update entry points share one in-flight check', updateCheckCallCount === 1 && coordinatedUpdateBusy === 'true|true|Prüfe…|true|true|true'); updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false, error: 'Simulierter Netzwerkfehler' })); await new Promise(resolve => setTimeout(resolve, 150)); const coordinatedUpdateError = await wc.executeJavaScript('(() => { const manual = document.getElementById("manualUpdateCheckBtn"); const header = document.getElementById("headerUpdateBtn"); return [manual?.disabled, header?.disabled, header?.hidden, manual?.textContent?.trim(), document.getElementById("copyToast")?.textContent?.trim()].join("|"); })()'); check('Settings update check uses the shared error contract', coordinatedUpdateError === 'false|false|true|Nach Updates suchen|Updateprüfung fehlgeschlagen'); await wc.executeJavaScript('document.getElementById("copyToast")?.classList.remove("show")'); await wc.executeJavaScript('requestUpdateCheck(); true'); await new Promise(resolve => setTimeout(resolve, 100)); updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false })); await new Promise(resolve => setTimeout(resolve, 150)); const noUpdateHeaderVisibility = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [button?.hidden, getComputedStyle(button).display].join("|"); })()'); check('Successful no-update result keeps the header action hidden', noUpdateHeaderVisibility === 'true|none'); const settingsNavigation = await wc.executeJavaScript('(() => { const buttons = [...document.querySelectorAll(".settings-nav-button")]; return [buttons.length, buttons.map(button => button.textContent.trim()).join("|"), document.querySelector(".settings-nav-button.active")?.dataset.settingsPage, document.getElementById("settingsSearchInput")?.placeholder].join("::"); })()'); check('Settings use the task-based sidebar navigation', settingsNavigation === '8::Allgemein|Uploads|Automatik|Benachrichtigungen|Logs & Support|Fernsteuerung|Diagnose-Zugriff|Backup & Übertragen::allgemein::Einstellungen durchsuchen'); const settingsSidebarInformation = await wc.executeJavaScript('(() => { const feedback = document.getElementById("saveFeedback"); const sidebar = document.querySelector(".settings-sidebar"); const status = document.querySelector(".settings-sidebar-status"); return Boolean(feedback && sidebar?.contains(feedback) && status && !document.querySelector(".settings-header #saveFeedback")); })()'); check('Settings sidebar owns the persistent save information', settingsSidebarInformation === true); await captureVisual('03-settings.png'); await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'uploads\\']")?.click()'); const uploadSettingsState = await wc.executeJavaScript('(() => { const activePage = document.querySelector(".settings-subpage.active"); return [activePage?.dataset.subpage, activePage?.querySelector("h3")?.textContent.trim(), document.querySelector("label[for=removeFromQueueOnDoneInput]")?.textContent.trim(), document.getElementById("removeFromQueueOnDoneInput")?.closest(".settings-option")?.querySelector(".settings-option-description")?.textContent.trim()].join("|"); })()'); check('Upload completion behavior is immediately findable', uploadSettingsState === 'uploads|Upload-Verhalten|Nach Abschluss aus der Liste entfernen|Erfolgreich hochgeladene Dateien verschwinden automatisch aus der Upload-Liste.'); const settingsReadingWidth = await wc.executeJavaScript('(() => { const activePage = document.querySelector(".settings-subpage.active"); if (!activePage) return 0; return activePage.getBoundingClientRect().width; })()'); check('Active settings page keeps a readable content width', settingsReadingWidth > 0 && settingsReadingWidth <= 640); const settingsFrameFit = await wc.executeJavaScript('(() => { const view = document.getElementById("settings-view")?.getBoundingClientRect(); const status = document.getElementById("statusbar")?.getBoundingClientRect(); return Boolean(view && status && status.height > 0 && view.bottom <= status.top + 1 && status.bottom <= window.innerHeight + 1); })()'); check('Settings view and statusbar fit inside the viewport', settingsFrameFit === true); if (process.env.MHU_SETTINGS_SCREENSHOT) { const screenshotPage = process.env.MHU_SETTINGS_SCREENSHOT_PAGE; if (screenshotPage) await wc.executeJavaScript('document.querySelector("[data-settings-page=" + ' + JSON.stringify(screenshotPage) + ' + "]")?.click()'); await new Promise(resolve => setTimeout(resolve, 150)); const screenshot = await wc.capturePage(); fs.writeFileSync(process.env.MHU_SETTINGS_SCREENSHOT, screenshot.toPNG()); } const settingsSearchState = await wc.executeJavaScript('(() => { const search = document.getElementById("settingsSearchInput"); if (!search) return "missing"; search.value = "fertig"; search.dispatchEvent(new Event("input", { bubbles: true })); const visible = [...document.querySelectorAll(".settings-nav-button")].filter(button => !button.hidden); return [visible.map(button => button.dataset.settingsPage).join(","), document.querySelector(".settings-nav-button.active")?.dataset.settingsPage].join("|"); })()'); check('Settings search routes completion terms to Uploads first', settingsSearchState === 'uploads,benachrichtigungen|uploads'); const settingsSearchRecovery = await wc.executeJavaScript('(() => { const search = document.getElementById("settingsSearchInput"); search.value = "kein-passender-treffer"; search.dispatchEvent(new Event("input", { bubbles: true })); const emptyVisible = !document.getElementById("settingsSearchEmpty").hidden; search.value = ""; search.dispatchEvent(new Event("input", { bubbles: true })); return [emptyVisible, document.querySelector(".settings-subpage.active")?.dataset.subpage, document.getElementById("settingsSearchEmpty").hidden].join("|"); })()'); check('Clearing an empty settings search restores the current page', settingsSearchRecovery === 'true|uploads|true'); const filteredOnlineRestoreNavigation = await wc.executeJavaScript('(() => { const search = document.getElementById("settingsSearchInput"); search.value = "fertig"; search.dispatchEvent(new Event("input", { bubbles: true })); _handleMenuAction("online-backup-restore"); return [search.value, document.querySelector(".settings-nav-button.active")?.dataset.settingsPage, document.activeElement?.id].join("|"); })()'); check('Online restore navigation clears filters and opens Backup', filteredOnlineRestoreNavigation === '|backup|onlineBackupKeyInput'); const accountSettingsPointer = await wc.executeJavaScript('document.querySelector(".settings-hoster-pointer")?.textContent'); check('Hoster settings point to Accounts tab', accountSettingsPointer && accountSettingsPointer.includes('Accounts')); const parallel = await wc.executeJavaScript('document.getElementById("parallelUploadCountInput")?.value'); check('Global parallel uploads default 0', parallel === '0'); await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'backup\\']")?.click()'); const onlineBackupControls = await wc.executeJavaScript('["createOnlineBackupBtn", "onlineBackupKeyOutput", "copyOnlineBackupKeyBtn", "onlineBackupKeyInput", "restoreOnlineBackupBtn", "onlineBackupStatus"].every(id => Boolean(document.getElementById(id)))'); check('Online backup controls exist', onlineBackupControls); const onlineBackupKeyContract = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput")?.maxLength + "|" + document.getElementById("onlineBackupKeyInput")?.getAttribute("pattern")'); check('Online backup input enforces the 75-character MHU key format', onlineBackupKeyContract === '75|MHU2-[A-Za-z0-9_-]{70}'); const onlineBackupBridge = await wc.executeJavaScript('typeof window.api.createOnlineBackup + "|" + typeof window.api.restoreOnlineBackup'); check('Online backup uses a narrow preload bridge', onlineBackupBridge === 'function|function'); const invalidOnlineBackup = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput").value = "MHU2-short"; document.getElementById("onlineBackupKeyInput").dispatchEvent(new Event("input", { bubbles: true })); document.getElementById("restoreOnlineBackupBtn").disabled + "|" + document.getElementById("onlineBackupStatus").textContent'); check('Invalid online backup keys stay blocked with visible guidance', invalidOnlineBackup === 'true|Der Schlüssel muss exakt 75 Zeichen lang sein.'); const validOnlineBackup = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput").value = "MHU2-" + "A".repeat(70); document.getElementById("onlineBackupKeyInput").dispatchEvent(new Event("input", { bubbles: true })); document.getElementById("restoreOnlineBackupBtn").disabled'); check('Valid 75-character online backup keys enable restore', validOnlineBackup === false); const onlineRestoreNavigation = await wc.executeJavaScript('_handleMenuAction("online-backup-restore"); document.activeElement?.id + "|" + document.querySelector(".settings-nav-button.active")?.dataset.settingsPage'); check('Online restore menu opens the backup page and focuses the key', onlineRestoreNavigation === 'onlineBackupKeyInput|backup'); // Test save await wc.executeJavaScript('document.getElementById("saveSettingsBtn").click()'); let feedback = ''; for (let attempt = 0; attempt < 50; attempt++) { feedback = await wc.executeJavaScript('document.getElementById("saveFeedback")?.textContent'); if (feedback === 'Gespeichert!') break; await new Promise(r => setTimeout(r, 50)); } check('Save shows Gespeichert!', feedback === 'Gespeichert!'); const originalShowSaveDialog = dialog.showSaveDialog; const originalShowOpenDialog = dialog.showOpenDialog; const exportRacePath = path.join(app.getPath('userData'), 'ui-export-race.json'); dialog.showSaveDialog = async () => ({ canceled: false, filePath: exportRacePath }); const exportRaceConfig = await wc.executeJavaScript('window.api.getConfig()'); const exportRaceSettings = { ...(exportRaceConfig.globalSettings || {}), webhookUrl: 'https://export-race.invalid/current' }; blockedWriteMarker = 'https://export-race.invalid/current'; blockedWriteStarted = false; releaseBlockedWrite = null; const delayedExportSave = wc.executeJavaScript('window.api.saveGlobalSettings(' + JSON.stringify(exportRaceSettings) + ')'); for (let attempt = 0; attempt < 100 && !blockedWriteStarted; attempt++) await new Promise(resolve => setTimeout(resolve, 10)); let exportSettled = false; const pendingFileExport = wc.executeJavaScript('window.api.exportBackup()').then(result => { exportSettled = true; return result; }); await new Promise(resolve => setTimeout(resolve, 80)); const exportSettledBeforeWrite = exportSettled; releaseBlockedWrite?.(); const [exportSaveResult, exportResult] = await Promise.all([delayedExportSave, pendingFileExport]); const exportedRaceConfig = JSON.parse(fs.readFileSync(exportRacePath, 'utf-8')); check('File backup waits for the ConfigStore write queue', exportSaveResult === true && exportResult?.ok === true && exportSettledBeforeWrite === false && exportedRaceConfig.globalSettings.webhookUrl === 'https://export-race.invalid/current'); blockedWriteMarker = ''; try { fs.unlinkSync(exportRacePath); } catch {} const historyExportRacePath = path.join(app.getPath('userData'), 'ui-history-export-race.json'); dialog.showSaveDialog = async () => ({ canceled: false, filePath: historyExportRacePath }); activeConfigStore._historyMigrated = true; fs.writeFileSync(activeConfigStore.historyPath, '[]', 'utf-8'); blockedHistoryWriteMarker = 'ui-history-export-race'; blockedHistoryWriteStarted = false; releaseBlockedHistoryWrite = null; const delayedHistoryExportWrite = activeConfigStore.appendHistory({ id: 'ui-history-export-race', files: [] }); for (let attempt = 0; attempt < 100 && !blockedHistoryWriteStarted; attempt++) await new Promise(resolve => setTimeout(resolve, 10)); let historyExportSettled = false; const pendingHistoryExport = wc.executeJavaScript('window.api.exportBackup()').then(result => { historyExportSettled = true; return result; }); await new Promise(resolve => setTimeout(resolve, 80)); const historyExportSettledBeforeWrite = historyExportSettled; releaseBlockedHistoryWrite?.(); const [historyExportWriteResult, historyExportResult] = await Promise.all([delayedHistoryExportWrite.then(() => true), pendingHistoryExport]); check('File backup waits for the ConfigStore history queue', historyExportWriteResult === true && historyExportResult?.ok === true && historyExportSettledBeforeWrite === false); blockedHistoryWriteMarker = ''; try { fs.unlinkSync(historyExportRacePath); } catch {} const importRaceConfig = await wc.executeJavaScript('window.api.getConfig()'); for (const hoster of Object.keys(importRaceConfig.hosters || {})) importRaceConfig.hosters[hoster] = []; importRaceConfig.hosters['byse.sx'] = [{ id: 'ui-imported-race-account', enabled: true, authType: 'api', apiKey: 'ui-imported-key' }]; importRaceConfig.globalSettings = { ...(importRaceConfig.globalSettings || {}), alwaysOnTop: false, webhookUrl: 'https://import-race.invalid/imported' }; const importRacePath = path.join(app.getPath('userData'), 'ui-import-race.json'); fs.writeFileSync(importRacePath, JSON.stringify(importRaceConfig), 'utf-8'); dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [importRacePath] }); blockedHistoryWriteMarker = 'ui-history-import-race'; blockedHistoryWriteStarted = false; releaseBlockedHistoryWrite = null; const delayedHistoryImportWrite = activeConfigStore.appendHistory({ id: 'ui-history-import-race', files: [] }); for (let attempt = 0; attempt < 100 && !blockedHistoryWriteStarted; attempt++) await new Promise(resolve => setTimeout(resolve, 10)); blockedWriteMarker = 'ui-imported-race-account'; blockedWriteStarted = false; releaseBlockedWrite = null; const pendingImport = wc.executeJavaScript('window.api.importBackup()'); await new Promise(resolve => setTimeout(resolve, 80)); const importStartedBeforeHistoryWrite = blockedWriteStarted; releaseBlockedHistoryWrite?.(); await delayedHistoryImportWrite; for (let attempt = 0; attempt < 100 && !blockedWriteStarted; attempt++) await new Promise(resolve => setTimeout(resolve, 10)); const dryRunPruneResult = await wc.executeJavaScript('window.api.pruneHistory("all", { dryRun: true }).then(() => ({ ok: true }), error => ({ ok: false, error: error.message }))'); const staleSettings = { ...(importRaceConfig.globalSettings || {}), alwaysOnTop: true, webhookUrl: 'https://import-race.invalid/stale' }; const staleSave = wc.executeJavaScript('window.api.saveGlobalSettings(' + JSON.stringify(staleSettings) + ').then(() => ({ ok: true }), error => ({ ok: false, error: error.message }))'); const stalePrune = wc.executeJavaScript('window.api.pruneHistory("all").then(() => ({ ok: true }), error => ({ ok: false, error: error.message }))'); await new Promise(resolve => setTimeout(resolve, 80)); releaseBlockedWrite?.(); const [importResult, staleSaveResult, stalePruneResult] = await Promise.all([pendingImport, staleSave, stalePrune]); const configAfterImportRace = await wc.executeJavaScript('window.api.getConfig()'); check('Import drains the ConfigStore history queue before replacement', importStartedBeforeHistoryWrite === false && importResult?.ok === true); check('Import allows dry history previews but rejects mutating prune calls', dryRunPruneResult?.ok === true && stalePruneResult?.ok === false); check('Import rejects stale config saves for the complete transition', staleSaveResult?.ok === false && configAfterImportRace.hosters['byse.sx']?.[0]?.id === 'ui-imported-race-account' && configAfterImportRace.globalSettings.alwaysOnTop === false && configAfterImportRace.globalSettings.webhookUrl === 'https://import-race.invalid/imported'); blockedWriteMarker = ''; blockedHistoryWriteMarker = ''; const importEpochWrites = await wc.executeJavaScript(\`(async () => { beginConfigImport(); let releaseB; const barrier = new Promise(resolve => { releaseB = resolve; }); const base = { ...(config.globalSettings || {}) }; const first = saveGlobalSettingsTracked({ ...base, webhookUrl: 'https://import-epoch.invalid/a' }).then(() => ({ ok: true }), error => ({ ok: false, code: error.code })); const between = enqueueConfigWriteOperation(() => barrier).promise; const second = saveGlobalSettingsTracked({ ...base, webhookUrl: 'https://import-epoch.invalid/b' }).then(() => ({ ok: true }), error => ({ ok: false, code: error.code })); const firstResult = await first; endConfigImport(); releaseB(); await between; const secondResult = await second; return { firstResult, secondResult, gateOpen: configImportInProgress === false }; })()\`); check('Import epoch rejects queued writes even when the gate ends between them', importEpochWrites.firstResult?.code === 'CONFIG_WRITE_SUPERSEDED' && importEpochWrites.secondResult?.code === 'CONFIG_WRITE_SUPERSEDED' && importEpochWrites.gateOpen === true); const importEpochConfig = structuredClone(configAfterImportRace); for (const hoster of Object.keys(importEpochConfig.hosters || {})) importEpochConfig.hosters[hoster] = []; importEpochConfig.hosters['byse.sx'] = [{ id: 'ui-import-epoch-account', enabled: true, authType: 'api', apiKey: 'ui-import-epoch-key' }]; importEpochConfig.globalSettings = { ...(importEpochConfig.globalSettings || {}), webhookUrl: 'https://import-epoch.invalid/imported' }; const importEpochPath = path.join(app.getPath('userData'), 'ui-import-epoch.json'); fs.writeFileSync(importEpochPath, JSON.stringify(importEpochConfig), 'utf-8'); dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [importEpochPath] }); blockedWriteMarker = 'ui-import-epoch-account'; blockedWriteStarted = false; releaseBlockedWrite = null; const initialPendingQueueHandler = initialIpcHandlers.get('save-pending-queue'); let finalImportQueueStarted = false; let releaseFinalImportQueue = null; ipcMain.removeHandler('save-pending-queue'); ipcMain.handle('save-pending-queue', (event, pendingQueue) => { if (!finalImportQueueStarted) { finalImportQueueStarted = true; return new Promise((resolve, reject) => { releaseFinalImportQueue = () => Promise.resolve(initialPendingQueueHandler(event, pendingQueue)).then(resolve, reject); }); } return initialPendingQueueHandler(event, pendingQueue); }); const pendingImportEpoch = wc.executeJavaScript('doBackupImport()'); for (let attempt = 0; attempt < 100 && !blockedWriteStarted; attempt++) await new Promise(resolve => setTimeout(resolve, 10)); releaseBlockedWrite?.(); for (let attempt = 0; attempt < 100 && !finalImportQueueStarted; attempt++) await new Promise(resolve => setTimeout(resolve, 10)); const importStateDuringFinalQueuePersist = await wc.executeJavaScript('({ gateClosed: configImportInProgress, webhookUrl: config.globalSettings.webhookUrl, accountId: config.hosters["byse.sx"]?.[0]?.id })'); const staleImportSettings = wc.executeJavaScript('saveGlobalSettingsTracked({ ...(config.globalSettings || {}), webhookUrl: "https://import-epoch.invalid/stale-after-commit" }).then(() => ({ ok: true }), error => ({ ok: false, code: error.code }))'); const staleImportAccounts = wc.executeJavaScript('saveConfigTracked({ hosters: { ...(config.hosters || {}), "byse.sx": [{ id: "ui-stale-account", enabled: true, authType: "api", apiKey: "stale" }] } }).then(() => ({ ok: true }), error => ({ ok: false, code: error.code }))'); releaseFinalImportQueue?.(); const [staleImportSettingsResult, staleImportAccountsResult] = await Promise.all([staleImportSettings, staleImportAccounts]); await pendingImportEpoch; const configAfterImportEpoch = await wc.executeJavaScript('window.api.getConfig()'); check('Import keeps its gate closed through apply and final queue persistence', importStateDuringFinalQueuePersist.gateClosed === true && importStateDuringFinalQueuePersist.webhookUrl === 'https://import-epoch.invalid/imported' && importStateDuringFinalQueuePersist.accountId === 'ui-import-epoch-account'); check('Import rejects stale settings and account writes until the full transition finishes', staleImportSettingsResult.code === 'CONFIG_WRITE_SUPERSEDED' && staleImportAccountsResult.code === 'CONFIG_WRITE_SUPERSEDED' && configAfterImportEpoch.hosters['byse.sx']?.[0]?.id === 'ui-import-epoch-account' && configAfterImportEpoch.globalSettings.webhookUrl === 'https://import-epoch.invalid/imported'); restoreInitialIpcHandler('save-pending-queue'); const importPersistFailureConfig = structuredClone(configAfterImportEpoch); importPersistFailureConfig.globalSettings = { ...(importPersistFailureConfig.globalSettings || {}), webhookUrl: 'https://import-persist-failure.invalid/imported' }; const importPersistFailurePath = path.join(app.getPath('userData'), 'ui-import-persist-failure.json'); fs.writeFileSync(importPersistFailurePath, JSON.stringify(importPersistFailureConfig), 'utf-8'); dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [importPersistFailurePath] }); let importQueueFailureAttempts = 0; ipcMain.removeHandler('save-pending-queue'); ipcMain.handle('save-pending-queue', (event, pendingQueue) => { importQueueFailureAttempts++; if (importQueueFailureAttempts === 1) throw new Error('final queue persistence failed'); return initialPendingQueueHandler(event, pendingQueue); }); await wc.executeJavaScript('doBackupImport()'); const importAfterQueueFailure = await wc.executeJavaScript('({ webhookUrl: config.globalSettings.webhookUrl, gateOpen: configImportInProgress === false, toast: document.getElementById("copyToast")?.textContent || "" })'); await wc.executeJavaScript('flushConfigWrites()'); check('Committed import remains applied when final queue persistence fails', importAfterQueueFailure.webhookUrl === 'https://import-persist-failure.invalid/imported' && importAfterQueueFailure.gateOpen === true && importAfterQueueFailure.toast.includes('Warteschlange') && importQueueFailureAttempts >= 2); restoreInitialIpcHandler('save-pending-queue'); blockedWriteMarker = ''; blockedHistoryWriteMarker = ''; dialog.showSaveDialog = originalShowSaveDialog; dialog.showOpenDialog = originalShowOpenDialog; try { fs.unlinkSync(importRacePath); } catch {} try { fs.unlinkSync(importEpochPath); } catch {} try { fs.unlinkSync(importPersistFailurePath); } catch {} const occupiedRemotePortServer = net.createServer(); await new Promise((resolve, reject) => { occupiedRemotePortServer.once('error', reject); occupiedRemotePortServer.listen(0, resolve); }); const occupiedRemotePort = occupiedRemotePortServer.address().port; const configBeforeRemoteFailure = JSON.parse(fs.readFileSync(activeConfigStore.filePath, 'utf-8')); const alwaysOnTopAfterRemoteFailure = !Boolean(configBeforeRemoteFailure.globalSettings.alwaysOnTop); const occupiedRemoteSettings = { ...(configBeforeRemoteFailure.globalSettings.remote || {}), enabled: true, allowInput: true, port: occupiedRemotePort, token: 'ui-occupied-port-token' }; const remoteSaveOutcome = await wc.executeJavaScript('saveRemoteSettingsTracked(' + JSON.stringify(occupiedRemoteSettings) + ').then(value => ({ resolved: true, value }), error => ({ resolved: false, error: error.message }))'); const configAfterRemoteFailure = JSON.parse(fs.readFileSync(activeConfigStore.filePath, 'utf-8')); const followUpRemoteWrite = await wc.executeJavaScript('setAlwaysOnTopTracked(' + JSON.stringify(alwaysOnTopAfterRemoteFailure) + ').then(() => ({ ok: true }), error => ({ ok: false, error: error.message }))'); const configAfterRemoteFollowUp = JSON.parse(fs.readFileSync(activeConfigStore.filePath, 'utf-8')); const reportedRemoteRuntimeFailure = remoteSaveOutcome.resolved === true && remoteSaveOutcome.value?.saved === true && typeof remoteSaveOutcome.value.runtimeError === 'string' && remoteSaveOutcome.value.runtimeError.length > 0; check('Remote runtime activation failure preserves settings without poisoning later writes', configAfterRemoteFailure.globalSettings.remote?.enabled === true && configAfterRemoteFailure.globalSettings.remote?.port === occupiedRemotePort && reportedRemoteRuntimeFailure === true && followUpRemoteWrite.ok === true && configAfterRemoteFollowUp.globalSettings.alwaysOnTop === alwaysOnTopAfterRemoteFailure); await new Promise(resolve => occupiedRemotePortServer.close(resolve)); const restoredRemoteSettings = { ...occupiedRemoteSettings, enabled: false }; await wc.executeJavaScript('saveRemoteSettingsTracked(' + JSON.stringify(restoredRemoteSettings) + ').catch(() => {})'); await wc.executeJavaScript('setAlwaysOnTopTracked(' + JSON.stringify(Boolean(configBeforeRemoteFailure.globalSettings.alwaysOnTop)) + ').catch(() => {})'); const generatedRemoteSettings = await wc.executeJavaScript('saveRemoteSettingsTracked(' + JSON.stringify({ ...restoredRemoteSettings, enabled: true, token: '' }) + ')'); const generatedRemoteToken = generatedRemoteSettings?.settings?.token || ''; const canonicalRemoteAfterFullSave = await wc.executeJavaScript('(async () => { config.globalSettings = { ...(config.globalSettings || {}), remote: { ...' + JSON.stringify(restoredRemoteSettings) + ', enabled: false, token: "" } }; renderSettings(); const tokenInput = document.getElementById("remoteTokenInput"); if (tokenInput) tokenInput.value = ""; await saveSettings({ feedbackText: "Gespeichert" }); return config.globalSettings.remote?.token || ""; })()'); const configAfterGeneratedTokenSave = JSON.parse(fs.readFileSync(activeConfigStore.filePath, 'utf-8')); check('Full settings save preserves the canonical remote token', generatedRemoteToken.length > 0 && canonicalRemoteAfterFullSave === generatedRemoteToken && configAfterGeneratedTokenSave.globalSettings.remote?.token === generatedRemoteToken); await wc.executeJavaScript('saveRemoteSettingsTracked(' + JSON.stringify({ ...restoredRemoteSettings, enabled: false, token: generatedRemoteToken }) + ').catch(() => {})'); let directConfigWriteRelease = null; let directConfigWriteStarted = false; let rendererExportCalls = 0; ipcMain.removeHandler('save-config'); ipcMain.handle('save-config', () => { directConfigWriteStarted = true; return new Promise(resolve => { directConfigWriteRelease = () => resolve(true); }); }); ipcMain.removeHandler('export-backup'); ipcMain.handle('export-backup', () => { rendererExportCalls++; return { ok: true, path: 'ui-renderer-export.mhu' }; }); await wc.executeJavaScript('(() => { HOSTERS.forEach(name => { config.hosters[name] = []; }); config.hosters["byse.sx"] = [{ id: "ui-tracked-account", enabled: true, authType: "api", apiKey: "tracked-key" }]; accountStatuses = { "ui-tracked-account": { status: "ok", message: "Bereit" } }; renderAccounts(); toggleAccount("ui-tracked-account"); })()'); for (let attempt = 0; attempt < 100 && !directConfigWriteStarted; attempt++) await new Promise(resolve => setTimeout(resolve, 10)); const pendingRendererExport = wc.executeJavaScript('doBackupExport()'); await new Promise(resolve => setTimeout(resolve, 80)); const rendererExportBeforeWrite = rendererExportCalls; directConfigWriteRelease?.(); await pendingRendererExport; check('Renderer export awaits direct account writes', directConfigWriteStarted === true && rendererExportBeforeWrite === 0 && rendererExportCalls === 1); const failedSaveRepairOrder = []; const failedSavePayloads = []; let failedSaveAttempts = 0; let fullSnapshotRepairCalls = 0; ipcMain.removeHandler('save-global-settings'); ipcMain.handle('save-global-settings', (_event, payload) => { failedSavePayloads.push(payload); failedSaveAttempts++; if (failedSaveAttempts === 1) { failedSaveRepairOrder.push('failed-save'); throw new Error('tracked settings save failed'); } failedSaveRepairOrder.push('retry-same-save'); return true; }); ipcMain.removeHandler('save-config'); ipcMain.handle('save-config', () => { fullSnapshotRepairCalls++; failedSaveRepairOrder.push('full-snapshot'); return true; }); ipcMain.removeHandler('export-backup'); ipcMain.handle('export-backup', () => { failedSaveRepairOrder.push('export'); return { ok: true, path: 'ui-repaired-export.mhu' }; }); const failedSaveRepairAlert = await wc.executeJavaScript('(() => { const originalSettingsPayload = { ...(config.globalSettings || {}), alwaysOnTop: true, webhookUrl: "https://repair-export.invalid/original" }; config.globalSettings = originalSettingsPayload; window.__failedSaveRepairAlert = ""; window.alert = message => { window.__failedSaveRepairAlert = String(message); }; return saveGlobalSettingsTracked(originalSettingsPayload).catch(() => {}).then(() => { originalSettingsPayload.alwaysOnTop = false; originalSettingsPayload.webhookUrl = "https://repair-export.invalid/newer"; config.globalSettings = { ...originalSettingsPayload }; return doBackupExport(); }).then(() => window.__failedSaveRepairAlert); })()'); const failedSavePayloadRetriedExactly = failedSavePayloads.length === 2 && JSON.stringify(failedSavePayloads[0]) === JSON.stringify(failedSavePayloads[1]) && failedSavePayloads[1].alwaysOnTop === true && failedSavePayloads[1].webhookUrl === 'https://repair-export.invalid/original'; check('Renderer retries the exact failed settings save before export without a full snapshot', failedSavePayloadRetriedExactly && fullSnapshotRepairCalls === 0 && failedSaveRepairOrder.join('|') === 'failed-save|retry-same-save|export' && failedSaveRepairAlert === ''); restoreInitialIpcHandler('save-global-settings'); restoreInitialIpcHandler('save-pending-queue'); const initialSaveGlobalSettingsHandler = initialIpcHandlers.get('save-global-settings'); const settingsRunnerMarker = 'https://runner-recovery.invalid/current'; let settingsRunnerAttempts = 0; ipcMain.removeHandler('save-global-settings'); ipcMain.handle('save-global-settings', (event, payload) => { if (payload?.webhookUrl === settingsRunnerMarker) { settingsRunnerAttempts++; if (settingsRunnerAttempts === 1) throw new Error('transient settings runner failure'); } return initialSaveGlobalSettingsHandler(event, payload); }); const settingsRunnerRecovery = await wc.executeJavaScript('(() => { const input = document.getElementById("webhookUrlInput"); input.value = "' + settingsRunnerMarker + '"; return saveSettings({ feedbackText: "Gespeichert" }).then(() => ({ firstFailed: false }), () => ({ firstFailed: true })).then(async result => { try { await flushPendingSettingsSaves(); result.flushOk = true; } catch (error) { result.flushOk = false; result.error = error.message; } return result; }); })()'); const configAfterSettingsRunnerRecovery = JSON.parse(fs.readFileSync(activeConfigStore.filePath, 'utf-8')); check('Close flush recovers a rejected serialized settings runner', settingsRunnerRecovery.firstFailed === true && settingsRunnerRecovery.flushOk === true && settingsRunnerAttempts >= 2 && configAfterSettingsRunnerRecovery.globalSettings.webhookUrl === settingsRunnerMarker); restoreInitialIpcHandler('save-global-settings'); const retainedWriteOrder = []; let retainedOldAttempts = 0; ipcMain.removeHandler('save-global-settings'); ipcMain.handle('save-global-settings', (_event, payload) => { if (payload.webhookUrl === 'https://retry-queue.invalid/old') { retainedOldAttempts++; retainedWriteOrder.push('old-' + retainedOldAttempts); if (retainedOldAttempts < 3) throw new Error('old write still failing'); } else if (payload.webhookUrl === 'https://retry-queue.invalid/new') { retainedWriteOrder.push('new'); } return true; }); ipcMain.removeHandler('export-backup'); ipcMain.handle('export-backup', () => { retainedWriteOrder.push('export'); return { ok: true, path: 'ui-retained-write-export.mhu' }; }); await wc.executeJavaScript('(() => { const oldSettings = { ...(config.globalSettings || {}), webhookUrl: "https://retry-queue.invalid/old" }; const newSettings = { ...oldSettings, webhookUrl: "https://retry-queue.invalid/new" }; return saveGlobalSettingsTracked(oldSettings).catch(() => {}).then(() => saveGlobalSettingsTracked(newSettings).catch(() => {})).then(() => doBackupExport()); })()'); check('Renderer retains newer writes when an older retry fails again', retainedWriteOrder.join('|') === 'old-1|old-2|old-3|new|export'); const importGateRetryOrder = []; let importGateActive = true; ipcMain.removeHandler('save-global-settings'); ipcMain.handle('save-global-settings', () => { if (importGateActive) { importGateRetryOrder.push('blocked-stale'); throw new Error('Einstellungen werden gerade importiert'); } importGateRetryOrder.push('replayed-stale'); return true; }); ipcMain.removeHandler('set-always-on-top'); ipcMain.handle('set-always-on-top', () => { importGateRetryOrder.push('imported-state'); return true; }); await wc.executeJavaScript('saveGlobalSettingsTracked({ ...(config.globalSettings || {}), webhookUrl: "https://import-gate.invalid/stale" }).catch(() => {})'); importGateActive = false; await wc.executeJavaScript('setAlwaysOnTopTracked(false)'); check('Renderer never retries a settings write rejected by the import gate', importGateRetryOrder.join('|') === 'blocked-stale|imported-state'); const queueImportOrder = []; const queueImportSnapshots = []; let queueImportStarted = false; let releaseQueueImport = null; ipcMain.removeHandler('save-pending-queue'); ipcMain.handle('save-pending-queue', (_event, pendingQueue) => { queueImportOrder.push('queue-save'); queueImportSnapshots.push(pendingQueue); return true; }); ipcMain.removeHandler('import-backup'); ipcMain.handle('import-backup', () => { queueImportOrder.push('import'); queueImportStarted = true; return new Promise(resolve => { releaseQueueImport = () => resolve({ ok: false, canceled: true }); }); }); const pendingQueueImport = wc.executeJavaScript('(() => { queueJobs = [{ id: "ui-queue-flush", file: "C:/ui/queue-flush.bin", fileName: "queue-flush.bin", hoster: "byse.sx", status: "preview", bytesTotal: 1 }]; rebuildJobIndex(); persistQueueStateSoon(false); return doBackupImport(); })()'); for (let attempt = 0; attempt < 100 && !queueImportStarted; attempt++) await new Promise(resolve => setTimeout(resolve, 10)); const queueWriteDuringImport = await wc.executeJavaScript('(() => { queueJobs = [{ id: "ui-import-dialog-terminal", file: "C:/ui/import-dialog-terminal.bin", fileName: "import-dialog-terminal.bin", hoster: "byse.sx", status: "done", bytesTotal: 10, bytesUploaded: 10, result: { download_url: "https://example.invalid/done" } }]; rebuildJobIndex(); return persistQueueStateNow().then(() => ({ ok: true }), error => ({ ok: false, error: error.message })); })()'); const terminalClearedBeforeImportFinished = queueImportSnapshots.some(snapshot => snapshot === null); releaseQueueImport?.(); await pendingQueueImport; await new Promise(resolve => setTimeout(resolve, 100)); const terminalClearedAfterCancel = queueImportSnapshots.some(snapshot => snapshot === null); check('Import flushes the throttled queue save before invoking main', queueImportOrder[0] === 'queue-save' && queueImportOrder[1] === 'import'); check('Queue progress stays persistable while an import dialog is open', queueWriteDuringImport.ok === true && terminalClearedBeforeImportFinished === true && terminalClearedAfterCancel === true); const importedQueueMerge = await wc.executeJavaScript('(() => { queueJobs = [{ id: "ui-live-queue-after-import", file: "C:/ui/live-after-import.bin", fileName: "live-after-import.bin", hoster: "byse.sx", status: "queued", bytesTotal: 12 }]; rebuildJobIndex(); const imported = structuredClone(config); imported.globalSettings = { ...(imported.globalSettings || {}), webhookUrl: "https://queue-merge.invalid/imported", pendingQueue: { savedAt: 1, queueJobs: [{ id: "ui-stale-import-queue" }] } }; applyImportedConfig(imported, "Importiert"); return { webhookUrl: config.globalSettings.webhookUrl, ids: config.globalSettings.pendingQueue?.queueJobs?.map(job => job.id) || [] }; })()'); check('Imported settings keep the live local queue in renderer memory', importedQueueMerge.webhookUrl === 'https://queue-merge.invalid/imported' && importedQueueMerge.ids.join('|') === 'ui-live-queue-after-import'); await wc.executeJavaScript('document.getElementById("copyToast")?.classList.remove("show")'); console.log('\\n=== History View ==='); ipcMain.removeHandler('get-history'); ipcMain.handle('get-history', () => [{ timestamp: '2026-08-10T10:00:00.000Z', files: [ { name: 'ok.bin', results: [{ status: 'done', hoster: 'voe.sx', download_url: 'https://example.invalid/ok' }] }, { name: 'bad.bin', results: [{ status: 'error', hoster: 'byse.sx', error: 'Zugang abgelehnt' }] }, { name: 'stopped.bin', results: [{ status: 'aborted', hoster: 'doodstream.com' }] } ] }]); await wc.executeJavaScript('_historyDirty = true; document.querySelector(".tab[data-view=\\'history\\']").click()'); await new Promise(r => setTimeout(r, 1000)); // Wait for async loadHistory const historyActive = await wc.executeJavaScript('document.getElementById("history-view")?.classList.contains("active")'); check('History tab active', historyActive); const historyWorkspaceLayout = await wc.executeJavaScript('(() => { const view = document.getElementById("history-view"); const sidebar = view?.querySelector(":scope > .view-sidebar"); const main = view?.querySelector(":scope > .view-main"); if (!sidebar || !main) return false; const sidebarRect = sidebar.getBoundingClientRect(); const mainRect = main.getBoundingClientRect(); return sidebarRect.width > 0 && mainRect.width > 0 && sidebarRect.right <= mainRect.left; })()'); check('History view separates sidebar and main workspace', historyWorkspaceLayout === true); const historySidebarInformation = await wc.executeJavaScript('(() => { const sidebar = document.querySelector("#history-view > .view-sidebar")?.getBoundingClientRect(); const section = document.querySelector("#history-view .view-sidebar-section")?.getBoundingClientRect(); const retention = document.getElementById("historySidebarRetention")?.textContent?.trim(); return Boolean(sidebar && section && section.top >= sidebar.top + sidebar.height * 0.55 && retention === "Alles behalten"); })()'); check('History sidebar shows the active retention in its lower area', historySidebarInformation === true); const historyFrameFit = await wc.executeJavaScript('(() => { const view = document.getElementById("history-view")?.getBoundingClientRect(); const status = document.getElementById("statusbar")?.getBoundingClientRect(); return Boolean(view && status && status.height > 0 && view.bottom <= status.top + 1 && status.bottom <= window.innerHeight + 1); })()'); check('History view and statusbar fit inside the viewport', historyFrameFit === true); await captureVisual('04-history.png'); const emptyState = await wc.executeJavaScript('document.querySelector("#historyContainer .empty-state")?.textContent'); check('Empty state or history table shown', emptyState === 'Noch keine Uploads.' || emptyState === undefined); const historyFilterState = await wc.executeJavaScript(\`(() => { const inspect = (value) => { document.querySelector('[data-history-filter="' + value + '"]').click(); return { rows: [...document.querySelectorAll('#historyBody .history-row')].map(row => row.querySelector('.col-filename')?.textContent).sort(), errors: document.querySelectorAll('#historyBody .history-row.error').length, pressed: [...document.querySelectorAll('[data-history-filter]')].filter(button => button.getAttribute('aria-pressed') === 'true').map(button => button.dataset.historyFilter), active: [...document.querySelectorAll('[data-history-filter].active')].map(button => button.dataset.historyFilter) }; }; const initial = { data: historyRowsData.length, rows: [...document.querySelectorAll('#historyBody .history-row')].map(row => row.querySelector('.col-filename')?.textContent).sort(), errors: document.querySelectorAll('#historyBody .history-row.error').length, counts: ['historySidebarAllCount', 'historySidebarSuccessCount', 'historySidebarErrorCount'].map(id => document.getElementById(id)?.textContent) }; const success = inspect('success'); const error = inspect('error'); const all = inspect('all'); return { initial, success, error, all, sourceLength: historyRowsData.length }; })()\`); check('History keeps failed results in the renderer data model and All view', historyFilterState.initial.data === 3 && historyFilterState.initial.rows.join('|') === 'bad.bin|ok.bin|stopped.bin' && historyFilterState.initial.errors === 2 && historyFilterState.initial.counts.join('|') === '3|1|2'); check('History sidebar filters successful and failed rows without dropping source data', historyFilterState.success.rows.join('|') === 'ok.bin' && historyFilterState.success.errors === 0 && historyFilterState.error.rows.join('|') === 'bad.bin|stopped.bin' && historyFilterState.error.errors === 2 && historyFilterState.all.rows.length === 3 && historyFilterState.sourceLength === 3); check('History sidebar exposes exactly one pressed filter', historyFilterState.success.pressed.join('|') === 'success' && historyFilterState.success.active.join('|') === 'success' && historyFilterState.error.pressed.join('|') === 'error' && historyFilterState.error.active.join('|') === 'error' && historyFilterState.all.pressed.join('|') === 'all' && historyFilterState.all.active.join('|') === 'all'); const historyErrorContrast = await wc.executeJavaScript(\`(() => { document.querySelector('[data-history-filter="error"]').click(); const row = document.querySelector('#historyBody .history-row.error'); if (!row) return 0; const parse = value => (value.match(/[0-9.]+/g) || []).slice(0, 3).map(Number); const foreground = parse(getComputedStyle(row).color); let node = row; let background = [32, 32, 32]; while (node) { const value = getComputedStyle(node).backgroundColor; if (value && value !== 'rgba(0, 0, 0, 0)' && value !== 'transparent') { background = parse(value); break; } node = node.parentElement; } const opacity = Number(getComputedStyle(row).opacity); const effective = foreground.map((channel, index) => channel * opacity + background[index] * (1 - opacity)); const luminance = rgb => { const values = rgb.map(channel => { const value = channel / 255; return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; }); return 0.2126 * values[0] + 0.7152 * values[1] + 0.0722 * values[2]; }; const foregroundLuminance = luminance(effective); const backgroundLuminance = luminance(background); return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05); })()\`); check('Failed history rows keep readable text contrast (' + historyErrorContrast.toFixed(2) + ':1)', historyErrorContrast >= 4.5); console.log('\\n=== Global UI ==='); const shutdownHidden = await wc.executeJavaScript('document.getElementById("shutdownOverlay")?.style.display'); check('Shutdown overlay hidden', shutdownHidden === 'none'); const toastHidden = await wc.executeJavaScript('!document.getElementById("copyToast")?.classList.contains("show")'); check('Copy toast hidden', toastHidden); const updateHidden = await wc.executeJavaScript('document.getElementById("updateBanner")?.style.display'); check('Update banner hidden', updateHidden === 'none'); const queueProgressVisibility = {}; for (const [label, width, height] of [['standard', 1100, 750], ['minimum', 800, 550]]) { win.setSize(width, height); await new Promise(resolve => setTimeout(resolve, 150)); queueProgressVisibility[label] = await wc.executeJavaScript(\`(() => { document.querySelector('.tab[data-view="upload"]').click(); selectedFiles = []; queueJobs = [{ id: 'ui-responsive', file: 'C:/ui/responsive.bin', fileName: 'responsive.bin', hoster: 'byse.sx', status: 'uploading', bytesUploaded: 512, bytesTotal: 1024, speedKbs: 64, elapsed: 5, remaining: 5, progress: 0.5 }]; rebuildJobIndex(); setUploadSidebarFilter('all'); updateUploadView(); renderQueueTable(); const container = document.getElementById('queueContainer'); container.scrollLeft = 0; const header = document.querySelector('#queueTable thead .col-progress'); const cell = document.querySelector('#queueBody .queue-row .col-progress'); const row = document.querySelector('#queueBody .queue-row'); const containerRect = container.getBoundingClientRect(); const headerRect = header?.getBoundingClientRect(); const cellRect = cell?.getBoundingClientRect(); return { headerVisible: Boolean(headerRect && headerRect.width > 0 && headerRect.left >= containerRect.left - 1 && headerRect.right <= containerRect.right + 1), cellVisible: Boolean(cellRect && cellRect.width > 0 && cellRect.left >= containerRect.left - 1 && cellRect.right <= containerRect.right + 1), headerHeight: headerRect?.height, rowHeight: row?.getBoundingClientRect().height }; })()\`); } const compactSettingsHeader = await wc.executeJavaScript('document.querySelector(".tab[data-view=settings]").click(); document.querySelector(".settings-header")?.getBoundingClientRect().height'); win.setBounds(originalBounds); await new Promise(resolve => setTimeout(resolve, 150)); await wc.executeJavaScript('queueJobs = []; rebuildJobIndex(); updateUploadView(); renderQueueTable(); updateStatusBar();'); check('Upload progress stays visible at the standard window size', queueProgressVisibility.standard.headerVisible && queueProgressVisibility.standard.cellVisible); check('Upload progress stays visible at the minimum window size', queueProgressVisibility.minimum.headerVisible && queueProgressVisibility.minimum.cellVisible); check('Responsive queue keeps a compact table header', queueProgressVisibility.standard.headerHeight <= 34 && queueProgressVisibility.minimum.headerHeight <= 34); check('Responsive queue keeps the fixed virtual row height', queueProgressVisibility.standard.rowHeight === 28 && queueProgressVisibility.minimum.rowHeight === 28); check('Minimum window keeps the settings header compact', compactSettingsHeader <= 58); const updateOverlayState = await wc.executeJavaScript('_knownUpdateInfo = { available: true, remoteVersion: "9.9.9" }; _syncHeaderUpdateState(); document.getElementById("headerUpdateBtn").focus(); showUpdateBanner({ remoteVersion: "9.9.9" }); (() => { const overlay = document.getElementById("updateBanner"); const dialog = overlay?.querySelector(".update-dialog"); const button = document.getElementById("headerUpdateBtn"); return [overlay?.classList.contains("update-overlay"), overlay?.style.display, dialog?.getAttribute("role"), dialog?.getAttribute("aria-modal"), button?.hidden, getComputedStyle(button).display].join("|"); })()'); check('Available update opens an accessible update dialog', updateOverlayState === 'true|flex|dialog|true|false|flex'); await new Promise(resolve => setTimeout(resolve, 100)); const updateModalKeyboard = await wc.executeJavaScript(\`(() => { const close = document.getElementById('updateCloseBtn'); const install = document.getElementById('installUpdateBtn'); const initialFocus = document.activeElement?.id; const backgroundInert = document.querySelector('.app-header')?.inert === true && document.querySelector('.view.active')?.inert === true && document.getElementById('statusbar')?.inert === true; install.focus(); install.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true })); const forwardFocus = document.activeElement?.id; close.focus(); close.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true, cancelable: true })); const backwardFocus = document.activeElement?.id; return { initialFocus, backgroundInert, forwardFocus, backwardFocus }; })()\`); check('Update dialog receives and traps keyboard focus', updateModalKeyboard.initialFocus === 'updateCloseBtn' && updateModalKeyboard.forwardFocus === 'updateCloseBtn' && updateModalKeyboard.backwardFocus === 'installUpdateBtn'); check('Update dialog makes the background inert', updateModalKeyboard.backgroundInert === true); await captureVisual('05-update.png'); const updateDialogCopy = await wc.executeJavaScript('(() => { const title = document.getElementById("updateDialogTitle")?.textContent?.trim(); const message = document.getElementById("updateMessage")?.textContent?.trim(); return [title, message].join("|"); })()'); check('Update dialog names the available version', updateDialogCopy === 'Eine neue Version ist verfügbar|Update v9.9.9 verfügbar'); const updateDialogActions = await wc.executeJavaScript('[document.getElementById("dismissUpdateBtn")?.textContent?.trim(), document.getElementById("installUpdateBtn")?.textContent?.trim()].join("|")'); check('Update dialog offers later and install actions', updateDialogActions === 'Später erinnern|Jetzt updaten'); const updateHeaderHint = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [button?.textContent?.trim(), button?.getAttribute("aria-label"), button?.dataset.tooltip].join("|"); })()'); check('Available update gives the header action a matching hint', updateHeaderHint === 'Update verfügbar|Update v9.9.9 verfügbar. Klicken zum Installieren.|Update v9.9.9 verfügbar. Klicken zum Installieren.'); const updateDialogDismissed = await wc.executeJavaScript('document.getElementById("dismissUpdateBtn")?.click(); (() => { const overlay = document.getElementById("updateBanner"); return [overlay?.style.display, overlay?.getAttribute("aria-hidden"), document.activeElement?.id, document.querySelector(".app-header")?.inert, document.querySelector(".view.active")?.inert, document.getElementById("statusbar")?.inert].join("|"); })()'); check('Update dialog closes and restores focus and background', updateDialogDismissed === 'none|true|headerUpdateBtn|false|false|false'); const busyUpdateState = await wc.executeJavaScript(\`(() => { showUpdateBanner({ remoteVersion: '9.9.9' }); handleUpdateProgress({ stage: 'downloading', percent: 50 }); const overlay = document.getElementById('updateBanner'); document.getElementById('updateCloseBtn').click(); document.getElementById('dismissUpdateBtn').click(); overlay.dispatchEvent(new MouseEvent('click', { bubbles: true })); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })); const progress = document.getElementById('updateProgressBar'); const header = document.getElementById('headerUpdateBtn'); return { display: overlay.style.display, hidden: overlay.getAttribute('aria-hidden'), closeDisabled: document.getElementById('updateCloseBtn').disabled, dismissDisabled: document.getElementById('dismissUpdateBtn').disabled, headerHidden: header.hidden, progressLabel: progress.getAttribute('aria-label'), progressText: progress.getAttribute('aria-valuetext') }; })()\`); check('Busy update keeps its progress dialog open', busyUpdateState.display === 'flex' && busyUpdateState.hidden === 'false' && busyUpdateState.closeDisabled === true && busyUpdateState.dismissDisabled === true && busyUpdateState.headerHidden === false); check('Update progress exposes an accessible live value', busyUpdateState.progressLabel === 'Update-Fortschritt' && busyUpdateState.progressText === 'Download 50%'); const updateErrorRecovery = await wc.executeJavaScript('handleUpdateProgress({ stage: "error", error: "Netzwerkfehler" }); document.getElementById("dismissUpdateBtn").click(); document.getElementById("updateBanner").style.display + "|" + document.getElementById("updateCloseBtn").disabled + "|" + document.getElementById("dismissUpdateBtn").disabled + "|" + document.getElementById("headerUpdateBtn").hidden'); check('Update errors restore all close actions', updateErrorRecovery === 'none|false|false|false'); const initialInstallUpdateHandler = initialIpcHandlers.get('app:install-update'); const initialUpdateQueueHandler = initialIpcHandlers.get('save-pending-queue'); let installUpdateIpcCalls = 0; ipcMain.removeHandler('app:install-update'); ipcMain.handle('app:install-update', () => { installUpdateIpcCalls++; return { started: true }; }); ipcMain.removeHandler('save-pending-queue'); ipcMain.handle('save-pending-queue', () => { throw new Error('update queue save failed'); }); const updateSaveFailure = await wc.executeJavaScript('showUpdateBanner({ remoteVersion: "9.9.9" }); installKnownUpdate().then(() => ({ busy: _updateInstallBusy, message: document.getElementById("updateMessage")?.textContent || "" }))'); check('Update preparation stops before install IPC when queue persistence fails', installUpdateIpcCalls === 0 && updateSaveFailure.busy === false && updateSaveFailure.message.includes('update queue save failed')); await wc.executeJavaScript('handleUpdateProgress({ stage: "error", error: "Test cleanup" }); closeUpdateDialog()'); ipcMain.removeHandler('app:install-update'); if (initialInstallUpdateHandler) registerIpcHandler('app:install-update', initialInstallUpdateHandler); ipcMain.removeHandler('save-pending-queue'); if (initialUpdateQueueHandler) registerIpcHandler('save-pending-queue', initialUpdateQueueHandler); await wc.executeJavaScript('flushConfigWrites()'); const stackedDialogState = await wc.executeJavaScript(\`(() => { openAccountModal(null); showUpdateBanner({ remoteVersion: '9.9.9' }); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })); const state = document.getElementById('updateBanner').style.display + '|' + document.getElementById('accountModal').style.display; if (document.getElementById('updateBanner').style.display !== 'none') closeUpdateDialog(); if (document.getElementById('accountModal').style.display !== 'none') closeAccountModal(); return state; })()\`); check('Escape closes only the topmost update dialog', stackedDialogState === 'none|flex'); if (visualScreenshotDir) { win.setSize(800, 550); await wc.executeJavaScript('document.querySelector(".tab[data-view=\\\'settings\\\']").click(); document.querySelector("[data-settings-page=\\\'allgemein\\\']")?.click(); (() => { const search = document.getElementById("settingsSearchInput"); if (search) { search.value = ""; search.dispatchEvent(new Event("input", { bubbles: true })); } document.querySelector(".settings-content")?.scrollTo(0, 0); })()'); await captureVisual('06-settings-800x550.png'); win.setBounds(originalBounds); } restoreInitialIpcHandler('save-global-settings'); restoreInitialIpcHandler('save-pending-queue'); const keepaliveWindow = new BrowserWindow({ show: false }); realAppQuit = app.quit.bind(app); app.relaunch = () => { relaunchCalls++; }; app.quit = () => { if (!win.isDestroyed()) win.close(); }; await wc.executeJavaScript('window.api.removeAllListeners()'); await wc.executeJavaScript('window.api.restartApp()'); await new Promise(resolve => setTimeout(resolve, 1700)); const lostCloseRequestRecovered = !win.isDestroyed(); await wc.executeJavaScript('window.api.onPrepareClose(prepareForWindowClose)'); await wc.executeJavaScript('window.api.onUpdateProgress(handleUpdateProgress)'); check('Lost restart close request leaves the window open for a later normal quit', lostCloseRequestRecovered === true); const originalSavePendingQueue = activeConfigStore.savePendingQueue.bind(activeConfigStore); let rejectHungFinalQueueWrite = null; let hangFinalQueueWrite = true; activeConfigStore.savePendingQueue = (pendingQueue, options) => { if (hangFinalQueueWrite && options?.allowDuringQuiesce === true) { hangFinalQueueWrite = false; return new Promise((_resolve, reject) => { rejectHungFinalQueueWrite = reject; }); } return originalSavePendingQueue(pendingQueue, options); }; const initialFinishCloseHandler = initialIpcHandlers.get('app:finish-close'); let closeReadyAttempt = null; let closeRestoreAttempt = null; let restoreAckRequested = false; let releaseRestoreAck = null; ipcMain.removeHandler('app:finish-close'); ipcMain.handle('app:finish-close', async (event, payload) => { const ready = payload && typeof payload === 'object' ? payload.ready !== false : payload !== false; if (ready) { closeReadyAttempt = payload && typeof payload === 'object' ? payload.attempt : null; } else { closeRestoreAttempt = payload && typeof payload === 'object' ? payload.attempt : null; restoreAckRequested = true; await new Promise(resolve => { releaseRestoreAck = resolve; }); } return initialFinishCloseHandler(event, payload); }); await wc.executeJavaScript('(() => { queuePersistThrottle.cancel(); selectedFiles = []; queueJobs = [{ id: "ui-close-timeout-job", file: "C:/ui/close-timeout.bin", fileName: "close-timeout.bin", hoster: "byse.sx", status: "queued", bytesUploaded: 0, bytesTotal: 1, speedKbs: 0, elapsed: 0, remaining: 0, progress: 0 }]; rebuildJobIndex(); return true; })()'); await wc.executeJavaScript('showUpdateBanner({ remoteVersion: "9.9.9" }); installKnownUpdate()'); await waitUntil(() => restoreAckRequested, 4000); const recoveryBeforeAck = await wc.executeJavaScript('({ state: closePreparationState, promiseCleared: closePreparationPromise === null, overlayVisible: document.getElementById("shutdownOverlay")?.style.display === "flex", inertRetained: closePreparationInertState.length > 0 && closePreparationInertState.every(({ element }) => element.inert === true) })'); const windowStayedOpenAfterCloseFailure = !win.isDestroyed(); rejectHungFinalQueueWrite?.(new Error('final queue write timeout')); releaseRestoreAck?.(); const boundedCloseRecovery = await waitUntil(async () => { if (win.isDestroyed()) return null; const state = await wc.executeJavaScript('({ state: closePreparationState, promiseCleared: closePreparationPromise === null, overlayHidden: document.getElementById("shutdownOverlay")?.style.display === "none", inertRestored: closePreparationInertState.length === 0, failedWrites: failedConfigWriteOperations.length })'); return state.state === 'open' && state.promiseCleared ? state : null; }, 4000); activeConfigStore.savePendingQueue = originalSavePendingQueue; const closeRecoveryMarker = 'https://close-recovery.invalid/after-timeout'; const writeAfterCloseRecovery = await wc.executeJavaScript('saveGlobalSettingsTracked({ ...(config.globalSettings || {}), webhookUrl: "' + closeRecoveryMarker + '" }).then(() => ({ ok: true }), error => ({ ok: false, error: error.message }))'); const historyAfterCloseRecovery = await activeConfigStore.appendHistory({ id: 'ui-close-recovery-history', files: [] }).then(() => ({ ok: true }), error => ({ ok: false, error: error.message })); const configAfterCloseRecovery = JSON.parse(fs.readFileSync(activeConfigStore.filePath, 'utf-8')); const recoveredQueue = configAfterCloseRecovery.globalSettings.pendingQueue?.queueJobs || []; const failedUpdateUi = await wc.executeJavaScript('({ busy: _updateInstallBusy, message: document.getElementById("updateMessage")?.textContent || "" })'); const closeRecoveryEvidence = { windowStayedOpenAfterCloseFailure, recoveryBeforeAck, boundedCloseRecovery, closeReadyAttempt, closeRestoreAttempt, writesQuiesced: activeConfigStore._writesQuiesced, writeAfterCloseRecovery, historyAfterCloseRecovery, persistedWebhookUrl: configAfterCloseRecovery.globalSettings.webhookUrl, recoveredQueue: recoveredQueue.map(job => job.id), preparedUpdateMockCalls, launchedUpdateMockCalls, failedUpdateUi }; const closeRecoveryOk = windowStayedOpenAfterCloseFailure === true && recoveryBeforeAck.state === 'recovering' && recoveryBeforeAck.promiseCleared === false && recoveryBeforeAck.overlayVisible === true && recoveryBeforeAck.inertRetained === true && boundedCloseRecovery?.state === 'open' && boundedCloseRecovery.promiseCleared === true && boundedCloseRecovery.overlayHidden === true && boundedCloseRecovery.inertRestored === true && boundedCloseRecovery.failedWrites === 0 && activeConfigStore._writesQuiesced === false && Number.isInteger(closeReadyAttempt) && closeRestoreAttempt === closeReadyAttempt && writeAfterCloseRecovery.ok === true && historyAfterCloseRecovery.ok === true && configAfterCloseRecovery.globalSettings.webhookUrl === closeRecoveryMarker && recoveredQueue.some(job => job.id === 'ui-close-timeout-job') && preparedUpdateMockCalls === 1 && launchedUpdateMockCalls === 0 && failedUpdateUi.busy === false && failedUpdateUi.message.includes('nicht gestartet'); if (!closeRecoveryOk) console.log('Close recovery evidence: ' + JSON.stringify(closeRecoveryEvidence)); check('Close recovery waits for its correlated restore ACK and drains retained writes before reopening', closeRecoveryOk); restoreInitialIpcHandler('app:finish-close'); const closeSnapshotWebhook = 'https://close-persist.invalid/current'; const closeSnapshotJobId = 'ui-close-persist-job'; activeConfigStore._historyMigrated = true; blockedHistoryWriteMarker = 'ui-close-history-write'; blockedHistoryWriteStarted = false; releaseBlockedHistoryWrite = null; const pendingCloseHistoryWrite = activeConfigStore.appendHistory({ id: 'ui-close-history-write', files: [] }); await waitUntil(() => blockedHistoryWriteStarted); await wc.executeJavaScript('(() => { config.globalSettings = { ...(config.globalSettings || {}), webhookUrl: "' + closeSnapshotWebhook + '" }; const webhookInput = document.getElementById("webhookUrlInput"); if (webhookInput) webhookInput.value = "' + closeSnapshotWebhook + '"; selectedFiles = []; queueJobs = [{ id: "' + closeSnapshotJobId + '", file: "C:/ui/close-persist.bin", fileName: "close-persist.bin", hoster: "byse.sx", status: "queued", bytesUploaded: 0, bytesTotal: 4096, speedKbs: 0, elapsed: 0, remaining: 0, progress: 0 }]; rebuildJobIndex(); scheduleSettingsSave(); persistQueueStateSoon(false); return true; })()'); let mainWindowClosed = false; win.once('closed', () => { mainWindowClosed = true; }); await wc.executeJavaScript('showUpdateBanner({ remoteVersion: "9.9.9" }); installKnownUpdate()'); const rendererCloseSealed = await waitUntil(async () => !win.isDestroyed() && await wc.executeJavaScript('closePreparationState === "sealed"')); const windowStayedOpenForClosePersistence = blockedHistoryWriteStarted === true && !win.isDestroyed(); const closeUiQuiesced = await wc.executeJavaScript('document.getElementById("shutdownOverlay")?.style.display === "flex" && closePreparationInertState.length > 0 && closePreparationInertState.every(({ element }) => element.inert === true)'); const postSealMainWrite = await wc.executeJavaScript('window.api.saveGlobalSettings({ ...(config.globalSettings || {}), webhookUrl: "https://close-persist.invalid/post-seal" }).then(() => ({ ok: true }), error => ({ ok: false, error: error.message }))'); await wc.executeJavaScript('(() => { queueJobs.push({ id: "ui-post-seal-job", file: "C:/ui/post-seal.bin", fileName: "post-seal.bin", hoster: "byse.sx", status: "queued", bytesUploaded: 0, bytesTotal: 1 }); rebuildJobIndex(); persistQueueStateSoon(false); scheduleSettingsSave(); return true; })()'); await new Promise(resolve => setTimeout(resolve, 650)); releaseBlockedHistoryWrite?.(); await pendingCloseHistoryWrite; await waitUntil(() => mainWindowClosed); const closePersistedConfig = JSON.parse(fs.readFileSync(activeConfigStore.filePath, 'utf-8')); const closePersistedQueue = closePersistedConfig.globalSettings.pendingQueue?.queueJobs || []; check('Window close waits for asynchronous persistence of the latest renderer snapshot', keepaliveWindow.isDestroyed() === false && blockedHistoryWriteStarted === true && windowStayedOpenForClosePersistence === true && mainWindowClosed === true && closePersistedConfig.globalSettings.webhookUrl === closeSnapshotWebhook && closePersistedQueue.some(job => job.id === closeSnapshotJobId)); check('Window close seals renderer writes before the final snapshot is committed', rendererCloseSealed === true && closeUiQuiesced === true && !closePersistedQueue.some(job => job.id === 'ui-post-seal-job')); check('Main rejects config producers after the close snapshot is sealed', postSealMainWrite.ok === false); } catch (err) { console.log('Test error:', err && err.stack ? err.stack : err.message); failed++; } const printResults = () => { console.log('\\n=== Results ==='); results.forEach(r => console.log(r)); console.log('\\nTotal: ' + (passed + failed) + ' | Passed: ' + passed + ' | Failed: ' + failed); }; if (realAppQuit) { app.once('will-quit', () => { check('Lost restart intent does not relaunch during the later normal quit', relaunchCalls === 0); check('Approved update launches its prepared installer exactly once', preparedUpdateMockCalls === 2 && launchedUpdateMockCalls === 1); printResults(); process.exitCode = failed > 0 ? 1 : 0; }); realAppQuit(); return; } printResults(); app.exit(failed > 0 ? 1 : 0); }, 5000); `; try { fs.writeFileSync(injectPath, testScript, 'utf-8'); execFileSync(process.execPath, ['--check', injectPath], { cwd: path.join(__dirname, '..'), stdio: 'pipe' }); const electronPath = path.join(__dirname, '..', 'node_modules', 'electron', 'dist', 'electron.exe'); const mainPath = path.join(__dirname, '..', 'main.js'); const result = execFileSync( electronPath, [`--user-data-dir=${userDataPath}`, '--require', injectPath, mainPath], { cwd: path.join(__dirname, '..'), timeout: 60000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } ); console.log(result); const isolatedConfigPath = path.join(userDataPath, 'electron-config.json'); if (!fs.existsSync(isolatedConfigPath)) throw new Error(`Isolated UI config was not created: ${isolatedConfigPath}`); } catch (err) { // timeout or exit code - still print output if (err.stdout) console.log(err.stdout); if (err.stderr) { const filtered = err.stderr.split('\n') .filter(l => !l.includes('cache_util') && !l.includes('disk_cache') && !l.includes('gpu_disk_cache')) .join('\n'); if (filtered.trim()) console.error(filtered); } process.exitCode = Number.isInteger(err.status) && err.status !== 0 ? err.status : 1; } finally { try { fs.unlinkSync(injectPath); } catch {} try { fs.rmSync(userDataPath, { recursive: true, force: true }); } catch {} }