release: Multi-Hoster-Upload 2.1.7

This commit is contained in:
Sucukdeluxe
2026-08-11 16:10:14 +02:00
parent f6b20a8bc8
commit 622460d844
28 changed files with 2368 additions and 80 deletions
+17 -6
View File
@@ -180,9 +180,8 @@ setTimeout(async () => {
check('English upload telemetry is fully localized', englishTelemetryLabels === 'Total|Connections|Remaining|Running|Completed|Failed|Speed|ETA');
const englishLayoutFits = await wc.executeJavaScript('(() => { const states = [...document.querySelectorAll(".tab")].map(tab => { tab.click(); const view = document.querySelector(".view.active"); return view && view.scrollWidth <= view.clientWidth + 1; }); document.querySelector(".tab[data-view=upload]")?.click(); return states.every(Boolean) && document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1; })()');
check('English labels fit every main view without horizontal overflow', englishLayoutFits === true);
await wc.executeJavaScript('document.querySelector(".tab[data-view=accounts]")?.click()');
const hiddenAccountSparkline = await wc.executeJavaScript('document.getElementById("uploadSpeedSparkline")?.classList.contains("is-hidden")');
check('Upload speed sparkline stays reserved but hidden outside Uploads', hiddenAccountSparkline === true);
const speedSparklineAcrossTabs = await wc.executeJavaScript('(() => [...document.querySelectorAll(".tab")].map(tab => { tab.click(); const widget = document.getElementById("uploadSpeedSparkline"); const rect = widget?.getBoundingClientRect(); const style = widget && getComputedStyle(widget); return Boolean(widget && !widget.classList.contains("is-hidden") && style.visibility === "visible" && style.opacity === "1" && rect.width > 0 && rect.height > 0); }))()');
check('Upload speed sparkline stays visible across every main tab', speedSparklineAcrossTabs.length === 4 && speedSparklineAcrossTabs.every(Boolean));
const englishEmptyAccountHosterLabel = await wc.executeJavaScript('(() => { const container = document.getElementById("accountsSidebarHosters"); return [container?.getAttribute("data-empty-label"), getComputedStyle(container, "::after").content].join("|"); })()');
check('Empty account hoster sidebar renders its localized English label', englishEmptyAccountHosterLabel === 'No hosts yet|"No hosts yet"');
await wc.executeJavaScript('document.querySelector(".tab[data-view=upload]")?.click()');
@@ -868,7 +867,7 @@ setTimeout(async () => {
const automationInputAlignment = await wc.executeJavaScript('(() => { const first = document.getElementById("autoRetryRoundsInput")?.getBoundingClientRect(); const second = document.getElementById("autoRetryDelayMinInput")?.getBoundingClientRect(); const firstHintEl = document.getElementById("autoRetryRoundsInput")?.closest(".automation-retry-row")?.querySelector(".hint"); const secondHintEl = document.getElementById("autoRetryDelayMinInput")?.closest(".automation-retry-row")?.querySelector(".hint"); const firstHint = firstHintEl?.getBoundingClientRect(); const secondHint = secondHintEl?.getBoundingClientRect(); if (!first || !second || !firstHint || !secondHint || !firstHintEl || !secondHintEl) return "missing"; const firstTextLeft = firstHint.left + parseFloat(getComputedStyle(firstHintEl).paddingLeft); const secondTextLeft = secondHint.left + parseFloat(getComputedStyle(secondHintEl).paddingLeft); return [Math.round(Math.abs(first.left - second.left)), Math.round(first.width), Math.round(second.width), firstHint.top >= first.bottom + 6, secondHint.top >= second.bottom + 6, Math.round(Math.abs(firstTextLeft - first.left)) <= 1, Math.round(Math.abs(secondTextLeft - second.left)) <= 1].join("|"); })()');
check('Automation retry hints start directly below their aligned inputs', automationInputAlignment === '0|100|100|true|true|true|true');
await wc.executeJavaScript('document.querySelector("[data-settings-page=allgemein]")?.click()');
const updateActionAlignment = await wc.executeJavaScript('(() => { const row = document.querySelector(".program-update-row")?.getBoundingClientRect(); const button = document.getElementById("manualUpdateCheckBtn")?.getBoundingClientRect(); return row && button ? [Math.abs(row.right - button.right) <= 12, button.bottom <= row.bottom, button.left > row.left + row.width / 2].join("|") : "missing"; })()');
const updateActionAlignment = await wc.executeJavaScript('(() => { const row = document.querySelector(".program-update-row")?.getBoundingClientRect(); const button = document.getElementById("manualUpdateCheckBtn")?.getBoundingClientRect(); return row && button ? [Math.abs(row.right - button.right) <= 16, button.bottom <= row.bottom, button.left > row.left + row.width / 2].join("|") : "missing"; })()');
check('Program update action sits at the lower right of its card', updateActionAlignment === 'true|true|true');
const updateCardContract = await wc.executeJavaScript('(() => { const card = document.querySelector(".program-update-card"); const title = card?.querySelector(".program-update-title"); const description = card?.querySelector(".program-update-description"); const button = document.getElementById("manualUpdateCheckBtn"); if (!card || !title || !description || !button) return "missing"; const cardRect = card.getBoundingClientRect(); const titleRect = title.getBoundingClientRect(); const descriptionRect = description.getBoundingClientRect(); const buttonRect = button.getBoundingClientRect(); const center = rect => rect.top + rect.height / 2; return [title.textContent.trim(), description.textContent.trim(), titleRect.top < descriptionRect.top, Math.abs(center(buttonRect) - center(cardRect)) <= 2, buttonRect.right <= cardRect.right - 10, titleRect.left >= cardRect.left + 10].join("|"); })()');
check('Program update card uses a clear title, description and vertically centered action', updateCardContract === 'Nach neuer Version suchen|Verfügbare Updates werden zusammen mit dem Changelog angezeigt.|true|true|true|true');
@@ -883,7 +882,7 @@ setTimeout(async () => {
const settingsSearchIconAlignment = await wc.executeJavaScript('(() => { const icon = document.querySelector(".settings-search-icon"); const style = getComputedStyle(icon); return [style.display, style.alignItems, Boolean(icon?.querySelector("svg"))].join("|"); })()');
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 === '40|40|0|0 0 24 24|18px|0px|0px|flex|center');
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');
await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'logs\\']")?.click()');
await new Promise(resolve => setTimeout(resolve, 100));
@@ -905,9 +904,21 @@ setTimeout(async () => {
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 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);
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); })()');
check('Enlarged settings pages stay horizontally contained', enlargedSettingsFit === true);
const sourceDeleteWarning = await wc.executeJavaScript('(() => { const option = document.querySelector(".source-delete-option"); const style = option && getComputedStyle(option); if (!style) return { visible: false }; const channels = style.borderTopColor.match(/[0-9.]+/g)?.map(Number) || []; return { visible: style.borderTopStyle === "solid" && parseFloat(style.borderTopWidth) >= 1, red: channels.length >= 3 && channels[0] > channels[1] * 1.25 && channels[0] > channels[2] * 1.15 }; })()');
check('Permanent source deletion is visibly framed as a danger setting', sourceDeleteWarning.visible && sourceDeleteWarning.red);
const sourceDeleteSafety = await wc.executeJavaScript('(async () => { const input = document.getElementById("deleteSourceAfterSuccessfulUploadInput"); const initial = input.checked; input.click(); await new Promise(resolve => setTimeout(resolve, 25)); const dialog = document.getElementById("appAlertModal"); const visible = dialog.style.display === "flex"; const danger = document.getElementById("appAlertConfirmBtn").classList.contains("btn-danger"); document.getElementById("appAlertCancelBtn").click(); await new Promise(resolve => setTimeout(resolve, 25)); return { initial, visible, danger, cancelled: input.checked === false }; })()');
check('Permanent source deletion defaults off and requires a danger confirmation', sourceDeleteSafety.initial === false && sourceDeleteSafety.visible && sourceDeleteSafety.danger && sourceDeleteSafety.cancelled);
const sourceDeletePersistence = await wc.executeJavaScript('(async () => { const input = document.getElementById("deleteSourceAfterSuccessfulUploadInput"); input.click(); await new Promise(resolve => setTimeout(resolve, 25)); document.getElementById("appAlertConfirmBtn").click(); await new Promise(resolve => setTimeout(resolve, 25)); const dirty = document.getElementById("saveSettingsBtn").disabled === false; await saveSettings({ feedbackText: "Gespeichert" }); const enabled = (await window.api.getGlobalSettings()).deleteSourceAfterSuccessfulUpload === true; input.checked = false; input.dispatchEvent(new Event("change", { bubbles: true })); await saveSettings({ feedbackText: "Gespeichert" }); const restored = (await window.api.getGlobalSettings()).deleteSourceAfterSuccessfulUpload === false; return { dirty, enabled, restored }; })()');
check('Confirmed source deletion persists and can be safely disabled again', sourceDeletePersistence.dirty && sourceDeletePersistence.enabled && sourceDeletePersistence.restored);
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 <= 760);
check('Active settings page keeps a readable content width', settingsReadingWidth > 0 && settingsReadingWidth <= 820);
const settingsFrameFit = await wc.executeJavaScript('(() => { const view = document.getElementById("settings-view")?.getBoundingClientRect(); return Boolean(view && view.bottom <= window.innerHeight + 1); })()');
check('Settings view fits inside the viewport', settingsFrameFit === true);