fix: stabilize the header on first paint

Pass the installed version with the startup language so the renderer can display it before asynchronous initialization.

Seed and start the upload sparkline immediately, keep a fixed update-action slot across all update states, and remove the delayed automatic update check.

Add first-frame regression coverage for version, speed baseline, update geometry, and startup query behavior.
This commit is contained in:
Sucukdeluxe
2026-08-20 13:27:55 +02:00
parent c4c7907686
commit 6d458e37f3
9 changed files with 83 additions and 25 deletions
+5 -1
View File
@@ -1,6 +1,10 @@
(function (root) { (function (root) {
'use strict'; 'use strict';
function createInitialSpeedHistoryState() {
return { display: 0, history: [0, 0] };
}
function updateSpeedHistory(state, target, maxSamples = 160) { function updateSpeedHistory(state, target, maxSamples = 160) {
const nextTarget = Number.isFinite(Number(target)) ? Math.max(0, Number(target)) : 0; const nextTarget = Number.isFinite(Number(target)) ? Math.max(0, Number(target)) : 0;
const previous = Number.isFinite(Number(state.display)) ? Math.max(0, Number(state.display)) : 0; const previous = Number.isFinite(Number(state.display)) ? Math.max(0, Number(state.display)) : 0;
@@ -14,7 +18,7 @@
return state; return state;
} }
const api = { updateSpeedHistory }; const api = { updateSpeedHistory, createInitialSpeedHistoryState };
if (typeof module !== 'undefined' && module.exports) module.exports = api; if (typeof module !== 'undefined' && module.exports) module.exports = api;
else if (root) root.SpeedHistory = api; else if (root) root.SpeedHistory = api;
})(typeof window !== 'undefined' ? window : this); })(typeof window !== 'undefined' ? window : this);
+6 -1
View File
@@ -8,6 +8,11 @@ function resolveStartupLanguage(config) {
return config && config.globalSettings && config.globalSettings.language === 'de' ? 'de' : 'en'; return config && config.globalSettings && config.globalSettings.language === 'de' ? 'de' : 'en';
} }
function createStartupQuery(config, version) {
const normalizedVersion = /^\d+\.\d+\.\d+$/.test(String(version || '').trim()) ? String(version).trim() : '';
return { language: resolveStartupLanguage(config), version: normalizedVersion };
}
function createStartupWindow(BrowserWindow, options) { function createStartupWindow(BrowserWindow, options) {
const window = new BrowserWindow({ ...options, show: false }); const window = new BrowserWindow({ ...options, show: false });
window.once('ready-to-show', () => { window.once('ready-to-show', () => {
@@ -22,4 +27,4 @@ function createStartupWindow(BrowserWindow, options) {
}; };
} }
module.exports = { configureStartupRenderer, createStartupWindow, resolveStartupLanguage }; module.exports = { configureStartupRenderer, createStartupWindow, resolveStartupLanguage, createStartupQuery };
+5 -5
View File
@@ -1,7 +1,7 @@
process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8'; process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8';
const { monitorEventLoopDelay, PerformanceObserver } = require('perf_hooks'); const { monitorEventLoopDelay, PerformanceObserver } = require('perf_hooks');
const { app, BrowserWindow, ipcMain, dialog, clipboard, nativeTheme, Tray, Menu, nativeImage } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, clipboard, nativeTheme, Tray, Menu, nativeImage } = require('electron');
const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage } = require('./lib/startup-renderer'); const { configureStartupRenderer, createStartupWindow, createStartupQuery } = require('./lib/startup-renderer');
configureStartupRenderer(app); configureStartupRenderer(app);
nativeTheme.themeSource = 'dark'; nativeTheme.themeSource = 'dark';
const path = require('path'); const path = require('path');
@@ -1508,12 +1508,12 @@ function createWindow() {
debugLog(`CHILD PROCESS GONE: type=${details.type} reason=${details.reason} exitCode=${details.exitCode}`); debugLog(`CHILD PROCESS GONE: type=${details.type} reason=${details.reason} exitCode=${details.exitCode}`);
}); });
let startupLanguage = 'en'; let startupQuery = createStartupQuery(null, app.getVersion());
try { startupLanguage = resolveStartupLanguage(configStore.load()); } catch {} try { startupQuery = createStartupQuery(configStore.load(), app.getVersion()); } catch {}
startupWindow.load(path.join(__dirname, 'renderer', 'index.html'), (err) => { startupWindow.load(path.join(__dirname, 'renderer', 'index.html'), (err) => {
_writeCrashLog('LOAD FILE FAILED', err); _writeCrashLog('LOAD FILE FAILED', err);
debugLog(`LOAD FILE FAILED: ${err && err.stack ? err.stack : err}`); debugLog(`LOAD FILE FAILED: ${err && err.stack ? err.stack : err}`);
}, { query: { language: startupLanguage } }); }, { query: startupQuery });
} }
function createTray() { function createTray() {
@@ -1652,7 +1652,7 @@ app.whenReady().then(async () => {
} }
} catch {} } catch {}
setTimeout(() => { void runAutomaticUpdateCheck(true); }, 3000); void runAutomaticUpdateCheck(true);
updateCheckInterval = setInterval(() => { void runAutomaticUpdateCheck(true); }, 5 * 60 * 1000); updateCheckInterval = setInterval(() => { void runAutomaticUpdateCheck(true); }, 5 * 60 * 1000);
updateCheckInterval.unref?.(); updateCheckInterval.unref?.();
}); });
+10 -3
View File
@@ -309,7 +309,7 @@ let settingsBaseline = '';
let settingsDirty = false; let settingsDirty = false;
let settingsSaving = false; let settingsSaving = false;
let lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: 0, elapsed: 0, activeJobs: 0 }; let lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: 0, elapsed: 0, activeJobs: 0 };
const uploadSpeedState = { display: 0, history: [] }; const uploadSpeedState = window.SpeedHistory.createInitialSpeedHistoryState();
let uploadSpeedTimer = null; let uploadSpeedTimer = null;
const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload'; const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload';
const QUEUE_COL_WIDTHS_KEY = 'queueColumnWidthsPx'; const QUEUE_COL_WIDTHS_KEY = 'queueColumnWidthsPx';
@@ -417,7 +417,6 @@ async function init() {
renderAccounts(); renderAccounts();
setupListeners(); setupListeners();
importEntryCoordinator.ready(); importEntryCoordinator.ready();
initUploadSpeedSparkline();
restoreQueueColumnWidths(); restoreQueueColumnWidths();
loadHistory(); loadHistory();
_refreshSessionFailedSnapshot(); _refreshSessionFailedSnapshot();
@@ -808,7 +807,6 @@ function _syncHeaderUpdateState() {
? `Update v${version || 'unbekannt'} verfügbar. Klicken zum Installieren.` ? `Update v${version || 'unbekannt'} verfügbar. Klicken zum Installieren.`
: 'Nach Aktualisierungen suchen'; : 'Nach Aktualisierungen suchen';
if (button) { if (button) {
button.hidden = !available;
button.classList.toggle('update-available', available); button.classList.toggle('update-available', available);
button.classList.toggle('is-checking', _updateCheckBusy); button.classList.toggle('is-checking', _updateCheckBusy);
button.disabled = _updateCheckBusy; button.disabled = _updateCheckBusy;
@@ -4195,6 +4193,14 @@ function initUploadSpeedSparkline() {
window.addEventListener('beforeunload', () => window.clearInterval(uploadSpeedTimer), { once: true }); window.addEventListener('beforeunload', () => window.clearInterval(uploadSpeedTimer), { once: true });
} }
function initializeStaticHeader() {
const version = new URLSearchParams(window.location.search).get('version') || '';
const versionLabel = document.getElementById('versionLabel');
if (versionLabel && /^\d+\.\d+\.\d+$/.test(version)) versionLabel.textContent = `v${version}`;
initUploadSpeedSparkline();
_syncHeaderUpdateState();
}
function updateStatusBar() { function updateStatusBar() {
const stats = _computeQueueStats(); const stats = _computeQueueStats();
@@ -8116,6 +8122,7 @@ function updateStatsPanel() {
window.api.onUpdateAvailable(showUpdateBanner); window.api.onUpdateAvailable(showUpdateBanner);
window.api.onUpdateProgress(handleUpdateProgress); window.api.onUpdateProgress(handleUpdateProgress);
window.api.onPrepareClose(prepareForWindowClose); window.api.onPrepareClose(prepareForWindowClose);
initializeStaticHeader();
setupDragDrop(); setupDragDrop();
init().then(() => { init().then(() => {
window.api.signalCloseHandshakeReady(); window.api.signalCloseHandshakeReady();
+1 -1
View File
@@ -58,7 +58,7 @@
<canvas id="uploadSpeedCanvas" width="232" height="44" aria-hidden="true"></canvas> <canvas id="uploadSpeedCanvas" width="232" height="44" aria-hidden="true"></canvas>
<strong id="uploadSpeedValue">0 B/s</strong> <strong id="uploadSpeedValue">0 B/s</strong>
</div> </div>
<button class="header-update-button" id="headerUpdateBtn" title="Nach Aktualisierungen suchen" aria-label="Nach Aktualisierungen suchen" data-tooltip="Nach Aktualisierungen suchen" hidden> <button class="header-update-button" id="headerUpdateBtn" title="Nach Aktualisierungen suchen" aria-label="Nach Aktualisierungen suchen" data-tooltip="Nach Aktualisierungen suchen">
<svg class="header-action-icon" aria-hidden="true"><use href="#icon-download"></use></svg> <svg class="header-action-icon" aria-hidden="true"><use href="#icon-download"></use></svg>
<span class="header-update-label">Update</span> <span class="header-update-label">Update</span>
</button> </button>
+17 -6
View File
@@ -2091,17 +2091,18 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
.header-update-button { .header-update-button {
position: relative; position: relative;
min-width: 106px; width: 146px;
flex: 0 0 146px;
height: 32px; height: 32px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 7px; gap: 7px;
padding: 0 12px; padding: 0 12px;
border: 1px solid transparent; border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
background: var(--accent); background: var(--bg-secondary);
color: var(--accent-ink); color: var(--text-muted);
cursor: pointer; cursor: pointer;
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
@@ -2109,8 +2110,10 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
transition: background-color .14s, transform .14s; transition: background-color .14s, transform .14s;
} }
.header-update-button[hidden] { .header-update-button.update-available {
display: none; border-color: transparent;
background: var(--accent);
color: var(--accent-ink);
} }
.header-update-button::after { .header-update-button::after {
@@ -2145,7 +2148,13 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
} }
.header-update-button:hover { .header-update-button:hover {
background: var(--bg-card-hover);
color: var(--text);
}
.header-update-button.update-available:hover {
background: var(--accent-end); background: var(--accent-end);
color: var(--accent-ink);
} }
.header-update-button:active { .header-update-button:active {
@@ -2276,6 +2285,7 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
} }
.version-badge { .version-badge {
min-width: 48px;
height: 32px; height: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -3987,6 +3997,7 @@ input[type="checkbox"] {
.header-update-button { .header-update-button {
min-width: 34px; min-width: 34px;
width: 34px; width: 34px;
flex: 0 0 34px;
padding: 0; padding: 0;
} }
+5 -1
View File
@@ -1,6 +1,10 @@
const test = require('node:test'); const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const { updateSpeedHistory } = require('../lib/speed-history'); const { updateSpeedHistory, createInitialSpeedHistoryState } = require('../lib/speed-history');
test('initial speed history contains a drawable zero baseline', () => {
assert.deepEqual(createInitialSpeedHistoryState(), { display: 0, history: [0, 0] });
});
test('speed history smooths rising and falling samples independently', () => { test('speed history smooths rising and falling samples independently', () => {
const state = { display: 0, history: [] }; const state = { display: 0, history: [] };
+28 -1
View File
@@ -3,7 +3,7 @@ const assert = require('node:assert/strict');
const { EventEmitter } = require('node:events'); const { EventEmitter } = require('node:events');
const fs = require('node:fs'); const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage } = require('../lib/startup-renderer'); const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage, createStartupQuery } = require('../lib/startup-renderer');
class TestBrowserWindow extends EventEmitter { class TestBrowserWindow extends EventEmitter {
constructor(options) { constructor(options) {
@@ -50,6 +50,14 @@ test('resolveStartupLanguage accepts only the supported persisted language', ()
assert.equal(resolveStartupLanguage(null), 'en'); assert.equal(resolveStartupLanguage(null), 'en');
}); });
test('startup query carries language and installed version into the first renderer frame', () => {
assert.deepEqual(createStartupQuery({ globalSettings: { language: 'de' } }, '2.1.25'), {
language: 'de',
version: '2.1.25'
});
assert.deepEqual(createStartupQuery(null, 'invalid'), { language: 'en', version: '' });
});
test('createStartupWindow forces the main window to start hidden', () => { test('createStartupWindow forces the main window to start hidden', () => {
const startup = createStartupWindow(TestBrowserWindow, { width: 1100, show: true }); const startup = createStartupWindow(TestBrowserWindow, { width: 1100, show: true });
@@ -124,3 +132,22 @@ test('upload sidebar renders and updates the remaining upload size', () => {
assert.match(html, /Verbleibende Größe[\s\S]*id="uploadTelemetryRemainingSize"[^>]*>0 B</u); assert.match(html, /Verbleibende Größe[\s\S]*id="uploadTelemetryRemainingSize"[^>]*>0 B</u);
assert.match(appSource, /_setUploadTelemetryText\(['"]uploadTelemetryRemainingSize['"],\s*formatBytes\(stats\.bytesRemaining\)\)/u); assert.match(appSource, /_setUploadTelemetryText\(['"]uploadTelemetryRemainingSize['"],\s*formatBytes\(stats\.bytesRemaining\)\)/u);
}); });
test('header occupies its final geometry before asynchronous initialization', () => {
const projectRoot = path.join(__dirname, '..');
const html = fs.readFileSync(path.join(projectRoot, 'renderer', 'index.html'), 'utf8');
const appSource = fs.readFileSync(path.join(projectRoot, 'renderer', 'app.js'), 'utf8');
const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8');
const css = fs.readFileSync(path.join(projectRoot, 'renderer', 'styles.css'), 'utf8');
const updateButton = html.match(/<button class="header-update-button"[^>]*id="headerUpdateBtn"[^>]*>/u)?.[0] || '';
const firstFrameInitialization = appSource.lastIndexOf('\ninitializeStaticHeader();');
const asynchronousInitialization = appSource.lastIndexOf('\ninit().then(');
assert.doesNotMatch(updateButton, /\shidden(?:\s|>)/u);
assert.match(css, /\.header-update-button\s*\{[^}]*width:\s*146px;[^}]*flex:\s*0 0 146px;/su);
assert.match(css, /\.version-badge\s*\{[^}]*min-width:\s*48px;/su);
assert.notEqual(firstFrameInitialization, -1);
assert.ok(firstFrameInitialization < asynchronousInitialization);
assert.match(mainSource, /createStartupQuery\([^,]+,\s*app\.getVersion\(\)\)/u);
assert.doesNotMatch(mainSource, /runAutomaticUpdateCheck\(true\);\s*\},\s*3000\)/u);
});
+6 -6
View File
@@ -190,7 +190,7 @@ setTimeout(async () => {
await wc.executeJavaScript('_knownUpdateInfo = null; closeUpdateDialog(); _syncHeaderUpdateState();'); await wc.executeJavaScript('_knownUpdateInfo = null; closeUpdateDialog(); _syncHeaderUpdateState();');
const germanStartupReady = await waitUntil(() => wc.executeJavaScript('document.documentElement.lang + "|" + document.getElementById("languageInput")?.value + "|" + [...document.querySelectorAll(".tab")].map(tab => tab.textContent.trim()).join(",")')); const germanStartupReady = await waitUntil(() => wc.executeJavaScript('document.documentElement.lang + "|" + document.getElementById("languageInput")?.value + "|" + [...document.querySelectorAll(".tab")].map(tab => tab.textContent.trim()).join(",")'));
check('Returning German profiles never expose an English frame while startup config is pending', startupLanguagePendingSnapshot !== null && (!startupLanguagePendingSnapshot.visible || startupLanguagePendingSnapshot.language === 'de') && startupLanguagePendingSnapshot.query === '?language=de' && germanStartupReady === 'de|de|Upload,Accounts,Einstellungen,Verlauf'); check('Returning German profiles never expose an English frame while startup config is pending', startupLanguagePendingSnapshot !== null && (!startupLanguagePendingSnapshot.visible || startupLanguagePendingSnapshot.language === 'de') && startupLanguagePendingSnapshot.query === ${JSON.stringify(`?language=de&version=${productVersion}`)} && germanStartupReady === 'de|de|Upload,Accounts,Einstellungen,Verlauf');
await wc.executeJavaScript('(async () => { config.globalSettings = { ...(config.globalSettings || {}), language: "en" }; await window.api.saveGlobalSettings(config.globalSettings); setUiLanguage("en"); renderSettings(); })()'); await wc.executeJavaScript('(async () => { config.globalSettings = { ...(config.globalSettings || {}), language: "en" }; await window.api.saveGlobalSettings(config.globalSettings); setUiLanguage("en"); renderSettings(); })()');
const languageReady = await waitUntil(() => wc.executeJavaScript('Boolean(document.getElementById("languageInput"))')); const languageReady = await waitUntil(() => wc.executeJavaScript('Boolean(document.getElementById("languageInput"))'));
check('Fresh profiles render in English by default', languageReady === true && await wc.executeJavaScript('document.documentElement.lang + "|" + document.getElementById("languageInput")?.value + "|" + [...document.querySelectorAll(".tab")].map(tab => tab.textContent.trim()).join(",")') === 'en|en|Upload,Accounts,Settings,History'); check('Fresh profiles render in English by default', languageReady === true && await wc.executeJavaScript('document.documentElement.lang + "|" + document.getElementById("languageInput")?.value + "|" + [...document.querySelectorAll(".tab")].map(tab => tab.textContent.trim()).join(",")') === 'en|en|Upload,Accounts,Settings,History');
@@ -270,7 +270,7 @@ setTimeout(async () => {
await saveSettings({ feedbackText: 'Gespeichert' }); await saveSettings({ feedbackText: 'Gespeichert' });
return { query: new URL(location.href).searchParams.get('language'), active: document.documentElement.lang }; return { query: new URL(location.href).searchParams.get('language'), active: document.documentElement.lang };
})()\`); })()\`);
check('Saved language remains the startup language after a renderer reload', englishLanguageQuery === 'en' && reloadedLanguageState === 'en|?language=en|Upload,Accounts,Settings,History' && germanLanguageQuery.query === 'de' && germanLanguageQuery.active === 'de'); check('Saved language remains the startup language after a renderer reload', englishLanguageQuery === 'en' && reloadedLanguageState === ${JSON.stringify(`en|?language=en&version=${productVersion}|Upload,Accounts,Settings,History`)} && germanLanguageQuery.query === 'de' && germanLanguageQuery.active === 'de');
await wc.executeJavaScript('queueJobs = []; selectedFiles = []; selectedJobIds.clear(); rebuildJobIndex(); setUploadSidebarFilter("all"); updateUploadView(); renderQueueTable(); updateStatusBar();'); await wc.executeJavaScript('queueJobs = []; selectedFiles = []; selectedJobIds.clear(); rebuildJobIndex(); setUploadSidebarFilter("all"); updateUploadView(); renderQueueTable(); updateStatusBar();');
console.log('\\n=== Upload View ==='); console.log('\\n=== Upload View ===');
@@ -291,7 +291,7 @@ setTimeout(async () => {
check('App header exposes the update action', headerUpdateButtonExists); 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("|"); })()'); 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'); check('Header update action keeps its layout slot before an update is available', initialHeaderUpdateVisibility === 'false|flex');
const initialUpdateLabel = await wc.executeJavaScript('document.querySelector("#headerUpdateBtn .header-update-label")?.textContent?.trim()'); const initialUpdateLabel = await wc.executeJavaScript('document.querySelector("#headerUpdateBtn .header-update-label")?.textContent?.trim()');
check('App header uses the compact update label', initialUpdateLabel === 'Update'); check('App header uses the compact update label', initialUpdateLabel === 'Update');
@@ -1199,11 +1199,11 @@ setTimeout(async () => {
})()\`); })()\`);
await new Promise(resolve => setTimeout(resolve, 100)); 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("|"); })()'); 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'); check('All update entry points share one in-flight check', updateCheckCallCount === 1 && coordinatedUpdateBusy === 'true|true|Prüfe…|true|true|false');
updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false, error: 'Simulierter Netzwerkfehler' })); updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false, error: 'Simulierter Netzwerkfehler' }));
await new Promise(resolve => setTimeout(resolve, 150)); 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("|"); })()'); 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'); check('Settings update check uses the shared error contract', coordinatedUpdateError === 'false|false|false|Nach Updates suchen|Updateprüfung fehlgeschlagen');
await wc.executeJavaScript('document.getElementById("copyToast")?.classList.remove("show")'); await wc.executeJavaScript('document.getElementById("copyToast")?.classList.remove("show")');
await wc.executeJavaScript('requestUpdateCheck(); true'); await wc.executeJavaScript('requestUpdateCheck(); true');
@@ -1211,7 +1211,7 @@ setTimeout(async () => {
updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false })); updateCheckResolvers.splice(0).forEach(resolve => resolve({ available: false }));
await new Promise(resolve => setTimeout(resolve, 150)); await new Promise(resolve => setTimeout(resolve, 150));
const noUpdateHeaderVisibility = await wc.executeJavaScript('(() => { const button = document.getElementById("headerUpdateBtn"); return [button?.hidden, getComputedStyle(button).display].join("|"); })()'); 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'); check('Successful no-update result keeps the header action in its stable slot', noUpdateHeaderVisibility === 'false|flex');
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("::"); })()'); 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'); check('Settings use the task-based sidebar navigation', settingsNavigation === '8::Allgemein|Uploads|Automatik|Benachrichtigungen|Logs & Support|Fernsteuerung|Diagnose-Zugriff|Backup & Übertragen::allgemein::Einstellungen durchsuchen');