Polish account interactions and settings layout
CI / verify (push) Successful in 5m2s

Add smooth account editor transitions with immediate input release, keep the cursor visible in development and production windows, align host option descriptions, and ensure localized settings search placeholders fit at narrow widths. Bump the application to v2.1.22 and extend hidden Electron regression coverage.
This commit is contained in:
Sucukdeluxe
2026-08-15 22:35:51 +02:00
parent f1774b50f8
commit 16f6f93a72
8 changed files with 269 additions and 18 deletions
+10 -2
View File
@@ -1,6 +1,10 @@
function configureStartupRenderer(app, env = process.env, platform = process.platform) {
function configureStartupRenderer(app, env = process.env, platform = process.platform, argv = process.argv) {
const sessionName = String(env && env.SESSIONNAME || '');
if (platform === 'win32' && /^RDP-/i.test(sessionName)) app.disableHardwareAcceleration();
if (Array.isArray(argv) && argv.includes('--dev')) {
app.commandLine.appendSwitch('force-prefers-no-reduced-motion');
app.commandLine.appendSwitch('user-data-dir', app.getPath('userData'));
}
return app;
}
@@ -516,7 +520,11 @@ function createStartupRendererHandlers({
}
function createStartupWindow(BrowserWindow, options) {
const window = new BrowserWindow({ ...options, show: false });
const window = new BrowserWindow({
...options,
show: false,
disableAutoHideCursor: true
});
return {
window,
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "multi-hoster-uploader",
"version": "2.1.21",
"version": "2.1.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-hoster-uploader",
"version": "2.1.21",
"version": "2.1.22",
"dependencies": {
"chokidar": "^3.6.0",
"undici": "^7.29.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "multi-hoster-uploader",
"version": "2.1.21",
"version": "2.1.22",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js",
"scripts": {
+44 -7
View File
@@ -5987,17 +5987,17 @@ function _buildHosterSettingsHtml(name) {
<input id="${fieldPrefix}-max-size" type="number" class="hs-input" data-hoster="${name}" data-hs="maxSizeMb" value="${hs.maxSizeMb ?? 0}" min="0">
<span class="hint">0 = unbegrenzt</span>
</div>
<div class="settings-row">
<div class="settings-row account-hoster-option-row">
<label for="${fieldPrefix}-log">Links in Log schreiben</label>
<input id="${fieldPrefix}-log" type="checkbox" class="hs-input" data-hoster="${name}" data-hs="logToFile" ${hs.logToFile !== false ? 'checked' : ''}>
<span class="hint">Erfolgreiche Links in fileuploader.log.</span>
</div>
<div class="settings-row">
<div class="settings-row account-hoster-option-row">
<label for="${fieldPrefix}-rotate">Accounts rotieren</label>
<input id="${fieldPrefix}-rotate" type="checkbox" class="hs-input" data-hoster="${name}" data-hs="rotateAccounts" ${hs.rotateAccounts === true ? 'checked' : ''}>
<span class="hint">Verteilt die Dateien reihum auf alle aktiven Accounts dieses Hosters (Datei 1 Account 1, Datei 2 Account 2 ). Hält z. B. byse-Accounts aktiv. Nur ein Account = kein Effekt.</span>
<span class="hint">Verteilt Dateien abwechselnd auf alle aktiven Accounts dieses Hosters. Bei nur einem Account hat die Option keinen Effekt.</span>
</div>
<div class="settings-row">
<div class="settings-row account-hoster-option-row">
<label for="${fieldPrefix}-size-memo">Größen-Limit merken</label>
<input id="${fieldPrefix}-size-memo" type="checkbox" class="hs-input" data-hoster="${name}" data-hs="sizeMemoEnabled" ${hs.sizeMemoEnabled !== false ? 'checked' : ''}>
<span class="hint">Überspringt nach zwei verdächtigen Ablehnungen auf einem Account größere Dateien dort vorab ("Bekanntes Größen-Limit"). Abschalten = jede Datei wird immer wirklich versucht.</span>
@@ -6353,7 +6353,7 @@ function getCredsFieldsHtml(authType, account, hoster) {
<div class="settings-row">
<label for="accField_password">Passwort</label>
<input type="password" class="key-input" id="accField_password" name="password" autocomplete="current-password" value="${escapeAttr(account.password || '')}" placeholder="Passwort">
<button class="toggle-vis" type="button" title="Passwort anzeigen" aria-label="Passwort anzeigen" aria-pressed="false">&#128065;</button>
<button class="toggle-vis" type="button" title="Passwort anzeigen" aria-label="Passwort anzeigen" aria-pressed="false">👁</button>
</div>`;
}
// API key
@@ -6361,7 +6361,7 @@ function getCredsFieldsHtml(authType, account, hoster) {
<div class="settings-row">
<label for="accField_apiKey">API-Key</label>
<input type="password" class="key-input" id="accField_apiKey" name="apiKey" autocomplete="off" spellcheck="false" value="${escapeAttr(account.apiKey || '')}" placeholder="API-Key">
<button class="toggle-vis" type="button" title="API-Key anzeigen" aria-label="API-Key anzeigen" aria-pressed="false">&#128065;</button>
<button class="toggle-vis" type="button" title="API-Key anzeigen" aria-label="API-Key anzeigen" aria-pressed="false">👁</button>
</div>`;
}
@@ -6429,13 +6429,50 @@ function openAccountModal(editAccountId) {
onEscape: closeAccountModal,
onBackdrop: closeAccountModal
});
_openAccountModalMotion(modal);
}
let _accountModalMotionToken = 0;
function _openAccountModalMotion(modal) {
const card = modal.querySelector('.modal-card');
const token = ++_accountModalMotionToken;
modal.classList.remove('account-modal-opening', 'account-modal-closing');
if (!card) return;
void card.offsetHeight;
modal.classList.add('account-modal-opening');
const finish = event => {
if (event && event.target !== card) return;
card.removeEventListener('animationend', finish);
if (Object.is(token, _accountModalMotionToken)) modal.classList.remove('account-modal-opening');
};
card.addEventListener('animationend', finish);
window.setTimeout(() => finish(), 440);
}
function closeAccountModal() {
modalController.close('accountModal', { fallbackFocus: '#addAccountBtn' });
const modal = document.getElementById('accountModal');
if (!modalController.isOpen(modal) || modal.classList.contains('account-modal-closing')) return;
const card = modal.querySelector('.modal-card');
const token = ++_accountModalMotionToken;
modal.classList.remove('account-modal-opening', 'account-modal-closing');
if (card) void card.offsetHeight;
modalController.close(modal, { fallbackFocus: '#addAccountBtn' });
modal.style.display = 'flex';
modal.inert = true;
modal.classList.add('account-modal-closing');
_hideOtpField();
editingAccountId = null;
_resetAccountModalState();
const finish = event => {
if (event && event.target !== card) return;
if (card) card.removeEventListener('animationend', finish);
if (!Object.is(token, _accountModalMotionToken)) return;
modal.classList.remove('account-modal-closing');
modal.style.display = 'none';
};
if (card) card.addEventListener('animationend', finish);
window.setTimeout(() => finish(), 380);
}
function openDeleteAccountModal(accountId) {
+1 -1
View File
@@ -237,7 +237,7 @@
['Hoster-Einstellungen', 'Host settings'],
['Upload-Einstellungen', 'Upload settings'],
['Erfolgreiche Links in fileuploader.log.', 'Successful links in fileuploader.log.'],
['Verteilt die Dateien reihum auf alle aktiven Accounts dieses Hosters (Datei 1 → Account 1, Datei 2 → Account 2 …). Hält z. B. byse-Accounts aktiv. Nur ein Account = kein Effekt.', 'Distributes files across all active accounts for this host in round-robin order (file 1 → account 1, file 2 → account 2, and so on). Keeps accounts such as byse active. Has no effect with only one account.'],
['Verteilt Dateien abwechselnd auf alle aktiven Accounts dieses Hosters. Bei nur einem Account hat die Option keinen Effekt.', 'Distributes files alternately across all active accounts for this host. The option has no effect with only one account.'],
['Größen-Limit merken', 'Remember size limit'],
['Überspringt nach zwei verdächtigen Ablehnungen auf einem Account größere Dateien dort vorab ("Bekanntes Größen-Limit"). Abschalten = jede Datei wird immer wirklich versucht.', 'After two suspicious rejections on an account, larger files are skipped there in advance ("Known size limit"). When disabled, every file is always attempted.'],
['Einstellungen einzelner Hoster', 'Settings for individual hosts'],
+66 -1
View File
@@ -29,6 +29,7 @@
body {
font-family: 'Aptos', 'Segoe UI Variable Text', 'Bahnschrift', sans-serif;
background: var(--bg-primary);
cursor: default;
height: 100vh;
overflow: hidden;
color: var(--text);
@@ -1287,6 +1288,29 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
.account-hoster-settings-header .panel-arrow { font-size: 10px; color: var(--text-dim); }
.account-hoster-settings-body-inner { padding: 4px 12px 10px; }
.account-hoster-settings-body-inner .account-hoster-option-row {
grid-column: 1 / -1;
display: grid;
grid-template-columns: 150px minmax(0, 1fr) auto;
align-items: center;
column-gap: 12px;
}
.account-hoster-settings-body-inner .account-hoster-option-row label {
min-width: 0;
}
.account-hoster-settings-body-inner .account-hoster-option-row .hint {
grid-column: 2;
line-height: 1.45;
}
.account-hoster-settings-body-inner .account-hoster-option-row input[type="checkbox"] {
grid-column: 3;
grid-row: 1;
justify-self: end;
}
.settings-hoster-pointer {
margin-top: 12px;
padding: 10px 14px;
@@ -3456,7 +3480,7 @@ input[type="checkbox"] {
}
#settingsSearchInput {
font-size: 13px;
font-size: 11px;
}
.settings-nav-button {
@@ -3753,6 +3777,47 @@ input[type="checkbox"] {
box-shadow: 0 24px 64px rgba(0, 0, 0, .48);
}
#accountModal.account-modal-opening {
animation: account-modal-dim-in 360ms cubic-bezier(.16, 1, .3, 1) both;
}
#accountModal.account-modal-opening > .modal-card {
transform-origin: top center;
animation: account-modal-unfold 360ms cubic-bezier(.16, 1, .3, 1) both;
}
#accountModal.account-modal-closing {
pointer-events: none;
animation: account-modal-dim-out 300ms cubic-bezier(.4, 0, .2, 1) both;
}
#accountModal.account-modal-closing > .modal-card {
transform-origin: top center;
animation: account-modal-fold 300ms cubic-bezier(.4, 0, .2, 1) both;
}
@keyframes account-modal-dim-in {
from { background: rgba(0, 0, 0, 0); }
to { background: rgba(0, 0, 0, .6); }
}
@keyframes account-modal-dim-out {
from { background: rgba(0, 0, 0, .6); }
to { background: rgba(0, 0, 0, 0); }
}
@keyframes account-modal-unfold {
0% { opacity: 0; clip-path: inset(0 0 18% 0 round 12px); transform: translateY(-16px) scale(.985); }
55% { opacity: .96; }
100% { opacity: 1; clip-path: inset(0 round 12px); transform: translateY(0) scale(1); }
}
@keyframes account-modal-fold {
0% { opacity: 1; clip-path: inset(0 round 12px); transform: translateY(0) scale(1); }
45% { opacity: .9; }
100% { opacity: 0; clip-path: inset(0 0 18% 0 round 12px); transform: translateY(-16px) scale(.985); }
}
.modal-header,
.modal-footer {
padding: 16px 18px;
+32 -1
View File
@@ -139,6 +139,32 @@ test('configureStartupRenderer disables hardware acceleration for a Windows Remo
assert.equal(calls, 1);
});
test('configureStartupRenderer forces full motion only for hot dev', () => {
const devSwitches = [];
const releaseSwitches = [];
const createApp = switches => ({
disableHardwareAcceleration() {},
getPath(name) {
assert.equal(name, 'userData');
return 'C:\\ReleaseTest\\user-data';
},
commandLine: {
appendSwitch(name, value) {
switches.push({ name, value });
}
}
});
configureStartupRenderer(createApp(devSwitches), { SESSIONNAME: 'Console' }, 'win32', ['electron', '.', '--dev']);
configureStartupRenderer(createApp(releaseSwitches), { SESSIONNAME: 'Console' }, 'win32', ['Multi-Hoster-Upload.exe']);
assert.deepEqual(devSwitches, [
{ name: 'force-prefers-no-reduced-motion', value: undefined },
{ name: 'user-data-dir', value: 'C:\\ReleaseTest\\user-data' }
]);
assert.deepEqual(releaseSwitches, []);
});
test('resolveStartupLanguage accepts only the supported persisted language', () => {
assert.equal(resolveStartupLanguage({ globalSettings: { language: 'de' } }), 'de');
assert.equal(resolveStartupLanguage({ globalSettings: { language: 'en' } }), 'en');
@@ -147,10 +173,15 @@ test('resolveStartupLanguage accepts only the supported persisted language', ()
});
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,
disableAutoHideCursor: false
});
assert.equal(startup.window.options.width, 1100);
assert.equal(startup.window.options.show, false);
assert.equal(startup.window.options.disableAutoHideCursor, true);
});
test('main window uses the branded application icon', () => {
+113 -3
View File
@@ -167,6 +167,7 @@ setTimeout(async () => {
if (win.isMaximized()) win.unmaximize();
const wc = win.webContents;
if (typeof wc.setFrameRate === 'function') wc.setFrameRate(60);
const initialReducedMotion = await wc.executeJavaScript('matchMedia("(prefers-reduced-motion: reduce)").matches');
if (!wc.debugger.isAttached()) wc.debugger.attach('1.3');
await wc.debugger.sendCommand('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'no-preference' }] });
const setWindowBounds = async bounds => {
@@ -218,6 +219,7 @@ setTimeout(async () => {
try {
check('Every UI smoke window stays offscreen and never takes focus', hiddenWindowHarness.areNativeSurfacesSuppressed(hiddenWindowHarness.getWindows()));
check('Hot Dev forces full motion despite the Windows reduced-motion preference', initialReducedMotion === false);
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();');
@@ -317,6 +319,9 @@ setTimeout(async () => {
const appHeaderExists = await wc.executeJavaScript('Boolean(document.querySelector(".app-header"))');
check('App shell exposes the primary header', appHeaderExists);
const cursorContract = await wc.executeJavaScript('[getComputedStyle(document.body).cursor, getComputedStyle(document.getElementById("settings-tab")).cursor].join("|")');
check('Main surface keeps a visible default cursor and interactive controls keep the pointer cursor', cursorContract === 'default|pointer');
const appBrandText = await wc.executeJavaScript('document.querySelector(".app-brand-name")?.textContent?.trim()');
check('App header shows the Multi Hoster Uploader brand', appBrandText === 'MULTI HOSTER UPLOADER');
@@ -980,15 +985,96 @@ setTimeout(async () => {
check('Password visibility action exposes its state', passwordToggleState === 'Passwort verbergen|true');
await wc.executeJavaScript('document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }))');
await new Promise(resolve => setTimeout(resolve, 340));
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; })()');
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 })); })()');
await new Promise(resolve => setTimeout(resolve, 340));
const fallbackAccountFocus = await wc.executeJavaScript('document.activeElement?.id');
check('Account modal restores stable focus after list rerender', fallbackAccountFocus === 'addAccountBtn');
ipcMain.removeHandler('validate-credentials');
ipcMain.handle('validate-credentials', () => ({ ok: true, status: 'ok', message: 'Credentials verified', checkedAt: '2033-01-02T03:04:05.000Z' }));
ipcMain.removeHandler('save-config');
ipcMain.handle('save-config', () => true);
await wc.executeJavaScript(\`(() => {
HOSTERS.forEach(name => { config.hosters[name] = []; });
config.hosters['byse.sx'] = [{ id: 'ui-animated-account', enabled: true, authType: 'api', apiKey: 'animated-key' }];
accountStatuses = { 'ui-animated-account': { status: 'ok', message: 'Ready' } };
renderAccounts();
document.querySelector('[data-account-edit="ui-animated-account"]')?.click();
})()\`);
await new Promise(resolve => setTimeout(resolve, 60));
const accountEditOpeningFrame = await wc.executeJavaScript(\`(() => {
const modal = document.getElementById('accountModal');
const card = modal?.querySelector('.modal-card');
const style = card ? getComputedStyle(card) : null;
return {
title: document.getElementById('accountModalTitle')?.textContent,
display: modal?.style.display,
animation: style?.animationName || 'none',
duration: parseFloat(style?.animationDuration || '0'),
transform: style?.transform || 'none',
clipPath: style?.clipPath || 'none',
opacity: Number(style?.opacity || 1),
overlayAnimation: getComputedStyle(modal).animationName,
overlayAlpha: Number((getComputedStyle(modal).backgroundColor.match(/[0-9.]+/g) || [0, 0, 0, 0])[3] || 0)
};
})()\`);
check('Editing an account unfolds the editor through a real opening frame', accountEditOpeningFrame.title === 'Account bearbeiten' && accountEditOpeningFrame.display === 'flex' && accountEditOpeningFrame.animation !== 'none' && accountEditOpeningFrame.duration >= .34 && (accountEditOpeningFrame.transform !== 'none' || accountEditOpeningFrame.clipPath !== 'none' || accountEditOpeningFrame.opacity < 1));
check('Opening the account editor softly fades in the surrounding dimming', accountEditOpeningFrame.overlayAnimation !== 'none' && accountEditOpeningFrame.overlayAlpha > 0 && accountEditOpeningFrame.overlayAlpha < .6);
await new Promise(resolve => setTimeout(resolve, 340));
const apiEyeEmoji = await wc.executeJavaScript('document.querySelector("#accountCredsFields .toggle-vis")?.textContent');
check('API key visibility uses the normal eye emoji', apiEyeEmoji === '👁️');
await wc.executeJavaScript('document.getElementById("closeAccountModalBtn")?.click()');
await new Promise(resolve => setTimeout(resolve, 60));
const accountEditClosingFrame = await wc.executeJavaScript(\`(() => {
const modal = document.getElementById('accountModal');
const card = modal?.querySelector('.modal-card');
const style = card ? getComputedStyle(card) : null;
return {
display: modal?.style.display,
ariaHidden: modal?.getAttribute('aria-hidden'),
modalInert: modal?.inert,
headerInert: document.querySelector('.app-header')?.inert,
activeViewInert: document.querySelector('.view.active')?.inert,
animation: style?.animationName || 'none',
duration: parseFloat(style?.animationDuration || '0'),
transform: style?.transform || 'none',
clipPath: style?.clipPath || 'none',
opacity: Number(style?.opacity || 1),
overlayAnimation: getComputedStyle(modal).animationName,
overlayAlpha: Number((getComputedStyle(modal).backgroundColor.match(/[0-9.]+/g) || [0, 0, 0, 0])[3] || 0)
};
})()\`);
check('The close button folds the account editor upward without blocking the application', accountEditClosingFrame.display === 'flex' && accountEditClosingFrame.ariaHidden === 'true' && accountEditClosingFrame.modalInert === true && accountEditClosingFrame.headerInert === false && accountEditClosingFrame.activeViewInert === false && accountEditClosingFrame.animation !== 'none' && accountEditClosingFrame.duration >= .28 && (accountEditClosingFrame.transform !== 'none' || accountEditClosingFrame.clipPath !== 'none' || accountEditClosingFrame.opacity < 1));
check('Closing the account editor softly fades out the surrounding dimming', accountEditClosingFrame.overlayAnimation !== 'none' && accountEditClosingFrame.overlayAlpha > 0 && accountEditClosingFrame.overlayAlpha < .6);
await new Promise(resolve => setTimeout(resolve, 340));
const accountEditClosed = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display === "none"');
check('The account editor hides after its closing animation', accountEditClosed === true);
await wc.executeJavaScript('document.querySelector("[data-account-edit=ui-animated-account]")?.click()');
await new Promise(resolve => setTimeout(resolve, 400));
await wc.executeJavaScript('document.getElementById("saveAccountBtn")?.click()');
await new Promise(resolve => setTimeout(resolve, 680));
const accountSaveClosingFrame = await wc.executeJavaScript(\`(() => {
const modal = document.getElementById('accountModal');
const card = modal?.querySelector('.modal-card');
const style = card ? getComputedStyle(card) : null;
return { display: modal?.style.display, animation: style?.animationName || 'none' };
})()\`);
check('A verified saved account uses the same closing animation', accountSaveClosingFrame.display === 'flex' && accountSaveClosingFrame.animation !== 'none');
await new Promise(resolve => setTimeout(resolve, 340));
const accountSaveClosed = await wc.executeJavaScript('document.getElementById("accountModal")?.style.display === "none"');
check('A verified saved account hides after the closing animation', accountSaveClosed === true);
restoreInitialIpcHandler('validate-credentials');
restoreInitialIpcHandler('save-config');
await setWindowBounds({ ...win.getBounds(), width: 1280, height: 720 });
const emptyAccountsGeometry = await wc.executeJavaScript(\`(() => {
@@ -1037,6 +1123,28 @@ setTimeout(async () => {
check('Hoster groups visibly animate while opening and closing', hasSmoothAccountCollapse(accountCollapseMotion.hosterMotion));
check('Hoster upload settings visibly animate while opening and closing', hasSmoothAccountCollapse(accountCollapseMotion.settingsMotion));
const accountSettingsDescriptionLayout = await wc.executeJavaScript(\`(async () => {
const hoster = HOSTERS[0];
const settings = document.querySelector('[data-hoster-settings-toggle="' + hoster + '"]');
if (settings && settings.getAttribute('aria-expanded') !== 'true') settings.click();
await new Promise(resolve => setTimeout(resolve, 240));
const container = document.querySelector('.account-hoster-settings-body-inner');
const rows = [...document.querySelectorAll('.account-hoster-option-row')];
const containerRect = container?.getBoundingClientRect();
return rows.map(row => {
const rowRect = row.getBoundingClientRect();
const hintRect = row.querySelector('.hint')?.getBoundingClientRect();
const inputRect = row.querySelector('input')?.getBoundingClientRect();
return {
widthRatio: containerRect ? rowRect.width / containerRect.width : 0,
descriptionBeforeToggle: Boolean(hintRect && inputRect && hintRect.right <= inputRect.left)
};
});
})()\`);
const accountSettingsDescriptionLayoutClean = accountSettingsDescriptionLayout.length === 3 && accountSettingsDescriptionLayout.every(row => row.widthRatio >= .97 && row.descriptionBeforeToggle);
if (!accountSettingsDescriptionLayoutClean) console.log('Account settings description layout:', JSON.stringify(accountSettingsDescriptionLayout));
check('Account host options give descriptions a full clean row with toggles aligned right', accountSettingsDescriptionLayoutClean);
const accountsFooterGeometry = await wc.executeJavaScript(\`(() => {
const main = document.querySelector('#accounts-view .accounts-main')?.getBoundingClientRect();
const list = document.getElementById('accountsList')?.getBoundingClientRect();
@@ -1394,6 +1502,8 @@ setTimeout(async () => {
check('Settings search icon aligns to the input text line', settingsSearchIconAlignment === 'flex|center|true');
const settingsSearchControlGeometry = await wc.executeJavaScript('(() => { const control = document.querySelector(".settings-search-control"); const input = document.getElementById("settingsSearchInput"); const icon = document.querySelector(".settings-search-icon"); const svg = icon?.querySelector("svg"); if (!control || !input || !icon || !svg) return "missing"; const controlRect = control.getBoundingClientRect(); const inputRect = input.getBoundingClientRect(); const iconRect = icon.getBoundingClientRect(); const inputStyle = getComputedStyle(input); const iconStyle = getComputedStyle(icon); return [Math.round(controlRect.height), Math.round(inputRect.height), Math.round(Math.abs((inputRect.top + inputRect.height / 2) - (iconRect.top + iconRect.height / 2))), svg.getAttribute("viewBox"), inputStyle.lineHeight, inputStyle.paddingTop, inputStyle.paddingBottom, iconStyle.display, iconStyle.alignItems].join("|"); })()');
check('Settings search control keeps icon and text on one shared center line', settingsSearchControlGeometry === '44|44|0|0 0 24 24|18px|0px|0px|flex|center');
const settingsSearchPlaceholderFit = await wc.executeJavaScript('(() => { const input = document.getElementById("settingsSearchInput"); if (!input) return false; const style = getComputedStyle(input); const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); context.font = style.font; const available = input.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); return ["Einstellungen durchsuchen", "Search settings"].every(text => context.measureText(text).width <= available - 12); })()');
check('Settings search placeholders retain visible right-side breathing room', settingsSearchPlaceholderFit === true);
await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'logs\\']")?.click()');
await new Promise(resolve => setTimeout(resolve, 100));
@@ -1454,7 +1564,7 @@ setTimeout(async () => {
const plaintextCredentialOverride = await wc.executeJavaScript('(() => ({ control: document.getElementById("allowPlaintextCredentialStorageInput"), copy: document.body.textContent.includes("Unsichere Klartext-Speicherung"), bridge: typeof window.api.getSecretStoreStatus }))()');
check('Settings expose no plaintext credential storage override', plaintextCredentialOverride.control === null && plaintextCredentialOverride.copy === false && plaintextCredentialOverride.bridge === 'undefined');
const settingsTypography = await wc.executeJavaScript('(() => { const size = selector => parseFloat(getComputedStyle(document.querySelector(selector)).fontSize); return { heading: size(".settings-subpage.active .settings-page-header h3"), intro: size(".settings-subpage.active .settings-page-header p"), section: size(".settings-subpage.active .settings-section-label"), rowLabel: size(".settings-subpage.active .settings-row > label"), hint: size(".settings-subpage.active .hint"), optionLabel: size(".settings-subpage.active .settings-option-copy label"), optionDescription: size(".settings-subpage.active .settings-option-description"), navigation: size(".settings-nav-button"), search: size("#settingsSearchInput") }; })()');
check('Settings use the enlarged readable typography scale', settingsTypography.heading >= 22 && settingsTypography.intro >= 14 && settingsTypography.section >= 12 && settingsTypography.rowLabel >= 14 && settingsTypography.hint >= 12 && settingsTypography.optionLabel >= 14 && settingsTypography.optionDescription >= 12 && settingsTypography.navigation >= 13 && settingsTypography.search >= 13);
check('Settings use the enlarged readable typography scale', settingsTypography.heading >= 22 && settingsTypography.intro >= 14 && settingsTypography.section >= 12 && settingsTypography.rowLabel >= 14 && settingsTypography.hint >= 12 && settingsTypography.optionLabel >= 14 && settingsTypography.optionDescription >= 12 && settingsTypography.navigation >= 13 && settingsTypography.search >= 11);
const settingsSelection = await wc.executeJavaScript('(() => ({ heading: getComputedStyle(document.querySelector(".settings-subpage.active .settings-page-header h3")).userSelect, hint: getComputedStyle(document.querySelector(".settings-subpage.active .hint")).userSelect, input: getComputedStyle(document.getElementById("globalMaxSpeedMbsInput")).userSelect }))()');
check('Settings interface copy cannot be selected while input values remain selectable', settingsSelection.heading === 'none' && settingsSelection.hint === 'none' && settingsSelection.input === 'text');
const enlargedSettingsFit = await wc.executeJavaScript('(() => { const results = [...document.querySelectorAll(".settings-nav-button")].map(button => { button.click(); const page = document.querySelector(".settings-subpage.active"); return Boolean(page && page.scrollWidth <= page.clientWidth + 1); }); document.querySelector("[data-settings-page=uploads]")?.click(); return results.every(Boolean); })()');
@@ -3564,7 +3674,7 @@ try {
const result = execFileSync(
electronPath,
[`--user-data-dir=${userDataPath}`, '--require', injectPath, mainPath],
[`--user-data-dir=${userDataPath}`, '--require', injectPath, mainPath, '--dev'],
{ cwd: path.join(__dirname, '..'), timeout: 180000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
);
console.log(result);