diff --git a/README.md b/README.md index c0cc8ea..deb8bc6 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,17 @@ Desktop downloader for Windows with package-based queue management, multi-provid ![Electron](https://img.shields.io/badge/Electron-31.x-47848F) ![React](https://img.shields.io/badge/React-18.x-149ECA) ![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6) -![License](https://img.shields.io/badge/license-MIT-green) - -## Why this tool? +![License](https://img.shields.io/badge/license-MIT-green) + +## Changelog + +### 2.0.6 + +- Keeps the download speed history alive when switching between tabs. +- Adds API and Web-Login filters to the account picker. +- Uses clearer Web-Login account labels and validates supported credentials before saving. + +## Why this tool? - JDownloader-style workflow with packages, progress, extraction, history, and clean post-processing. - Multiple debrid accounts in one app, including provider order, automatic fallback, and per-hoster routing. diff --git a/package-lock.json b/package-lock.json index e548dcf..6c2013a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "2.0.5", + "version": "2.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "2.0.5", + "version": "2.0.6", "license": "MIT", "dependencies": { "adm-zip": "0.6.0", diff --git a/package.json b/package.json index 274cdf3..172a1d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "2.0.5", + "version": "2.0.6", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 69cc17d..09d848d 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -546,16 +546,18 @@ export class AppController { return fetchDebridLinkHostLimits(this.settings.debridLinkApiKeys, host); } -public async checkDebridAccounts(): Promise { - const statuses = await checkAllDebridAccounts(this.settings); - this.manager.applyDebridAccountStatuses(statuses); - this.audit("INFO", "Debrid-Accounts geprueft", { - total: statuses.length, - valid: statuses.filter((s) => s.valid).length, - premium: statuses.filter((s) => s.isPremium).length - }); - return statuses; - } +public async checkDebridAccounts(settingsOverride?: AppSettings): Promise { + const statuses = await checkAllDebridAccounts(settingsOverride ? normalizeSettings(settingsOverride) : this.settings); + if (!settingsOverride) { + this.manager.applyDebridAccountStatuses(statuses); + this.audit("INFO", "Debrid-Accounts geprueft", { + total: statuses.length, + valid: statuses.filter((s) => s.valid).length, + premium: statuses.filter((s) => s.isPremium).length + }); + } + return statuses; + } public async checkSingleMegaDebridAccount(login: string, password: string): Promise { const entry = parseMegaDebridAccounts(`${login.trim()}:${password.trim()}`)[0]; diff --git a/src/main/main.ts b/src/main/main.ts index b208323..98da62c 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -747,9 +747,9 @@ function registerIpcHandlers(): void { return controller.getDebridLinkHostLimits(); }); - ipcMain.handle(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async () => { - return controller.checkDebridAccounts(); - }); + ipcMain.handle(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async (_event, settings?: AppSettings) => { + return controller.checkDebridAccounts(settings); + }); ipcMain.handle(IPC_CHANNELS.CHECK_MEGA_DEBRID_ACCOUNT, async (_event, login: string, password: string) => { return controller.checkSingleMegaDebridAccount(String(login || ""), String(password || "")); diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 54c76b1..380373c 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -87,7 +87,7 @@ const api: ElectronApi = { importBestDebridCookies: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES), getAllDebridHostInfo: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO), getDebridLinkHostLimits: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS), - checkDebridAccounts: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS), + checkDebridAccounts: (settings?: AppSettings): Promise => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, settings), checkMegaDebridAccount: (login: string, password: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.CHECK_MEGA_DEBRID_ACCOUNT, login, password), retryExtraction: (packageId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId), extractNow: (packageId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 4ca55e9..25709cb 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -36,8 +36,12 @@ import { getProviderDailyUsageBytes, getProviderUsageDayKey } from "../shared/provider-daily-limits"; -import { reorderPackageOrderByDrop, sortPackageOrderByName, sortPackagesForDisplay } from "./package-order"; -import { pruneSelection } from "./selection"; +import { reorderPackageOrderByDrop, sortPackageOrderByName, sortPackagesForDisplay } from "./package-order"; +import { pruneSelection } from "./selection"; +import { matchesAccountModeFilter } from "./account-ui"; +import type { AccountModeFilter } from "./account-ui"; +import { DOWNLOAD_SPEED_MAX_SAMPLES, updateDownloadSpeedHistory } from "./download-speed-state"; +import type { DownloadSpeedHistoryState } from "./download-speed-state"; type Tab = "collector" | "downloads" | "history" | "statistics" | "settings"; type SettingsSubTab = "allgemein" | "accounts" | "entpacken" | "geschwindigkeit" | "bereinigung" | "updates"; @@ -245,8 +249,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ kind: "realdebrid-web", service: "realdebrid", serviceLabel: "Real-Debrid", - title: "Real-Debrid Web", - modeLabel: "Web", + title: "Real-Debrid Web-Login", + modeLabel: "Web-Login", pickerDescription: "Login über Browserfenster statt Token." }, { @@ -262,8 +266,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ kind: "megadebrid-web", service: "megadebrid-web", serviceLabel: "Mega-Debrid", - title: "Mega-Debrid Web", - modeLabel: "Web", + title: "Mega-Debrid Web-Login", + modeLabel: "Web-Login", pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account.", needsToken: true }, @@ -280,8 +284,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ kind: "bestdebrid-web", service: "bestdebrid", serviceLabel: "BestDebrid", - title: "BestDebrid Web", - modeLabel: "Web", + title: "BestDebrid Web-Login", + modeLabel: "Web-Login", pickerDescription: "Cookie-Import aus dem Browser statt API-Token." }, { @@ -297,8 +301,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ kind: "alldebrid-web", service: "alldebrid", serviceLabel: "AllDebrid", - title: "AllDebrid Web", - modeLabel: "Web", + title: "AllDebrid Web-Login", + modeLabel: "Web-Login", pickerDescription: "Login über Browserfenster für reCAPTCHA.", }, { @@ -332,8 +336,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ kind: "linksnappy-login", service: "linksnappy", serviceLabel: "LinkSnappy", - title: "LinkSnappy Web", - modeLabel: "Web", + title: "LinkSnappy Web-Login", + modeLabel: "Web-Login", pickerDescription: "Login für linksnappy.com mit Benutzername und Passwort.", needsCredentials: true } @@ -802,8 +806,11 @@ function validateAccountDialog(dialog: AccountDialogState): string | null { return `${option.title}: Tageslimit muss eine Zahl >= 0 sein.`; } } - if (dialog.kind === "debridlink-api") { - for (const key of parseDebridLinkApiKeys(dialog.token)) { + if (dialog.kind === "debridlink-api") { + if (parseDebridLinkApiKeys(dialog.token).length === 0) { + return `${option.title}: Bitte mindestens einen gültigen API-Key eintragen.`; + } + for (const key of parseDebridLinkApiKeys(dialog.token)) { const raw = dialog.keyDailyLimitGbById?.[key.id] || ""; if (!raw.trim()) { continue; @@ -1482,20 +1489,18 @@ const BandwidthChart = memo(function BandwidthChart({ items, running, paused, sp ); }); -interface DownloadSpeedSparklineProps { - items: Record; - running: boolean; - paused: boolean; -} - -const SPARKLINE_MAX_SAMPLES = 160; - -const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, running, paused }: DownloadSpeedSparklineProps): ReactElement { - const canvasRef = useRef(null); - const histRef = useRef([]); - const displayRef = useRef(0); - const itemsRef = useRef(items); - const activeRef = useRef(running && !paused); +interface DownloadSpeedSparklineProps { + items: Record; + running: boolean; + paused: boolean; + speedStateRef: React.MutableRefObject; + hidden?: boolean; +} + +const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, running, paused, speedStateRef, hidden = false }: DownloadSpeedSparklineProps): ReactElement { + const canvasRef = useRef(null); + const itemsRef = useRef(items); + const activeRef = useRef(running && !paused); const [liveSpeed, setLiveSpeed] = useState(0); itemsRef.current = items; activeRef.current = running && !paused; @@ -1515,7 +1520,7 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, cssW, cssH); - const hist = histRef.current; + const hist = speedStateRef.current.history; if (hist.length < 2) return; const isDark = document.documentElement.getAttribute("data-theme") !== "light"; @@ -1528,8 +1533,8 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run const pad = 2; const h = cssH - pad * 2; - const step = cssW / (SPARKLINE_MAX_SAMPLES - 1); - const startIdx = SPARKLINE_MAX_SAMPLES - hist.length; + const step = cssW / (DOWNLOAD_SPEED_MAX_SAMPLES - 1); + const startIdx = DOWNLOAD_SPEED_MAX_SAMPLES - hist.length; const px = (i: number): number => (startIdx + i) * step; const py = (v: number): number => pad + h - (v / maxV) * h; @@ -1558,24 +1563,17 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run if (it.status === "downloading") target += it.speedBps || 0; } } - const cur = displayRef.current; - const alpha = target > cur ? 0.45 : 0.12; - let next = cur + (target - cur) * alpha; - if (next < 1) next = 0; - displayRef.current = next; - const hist = histRef.current; - hist.push(next); - if (hist.length > SPARKLINE_MAX_SAMPLES) hist.splice(0, hist.length - SPARKLINE_MAX_SAMPLES); - setLiveSpeed(target); - draw(); + speedStateRef.current = updateDownloadSpeedHistory(speedStateRef.current, target); + setLiveSpeed(target); + draw(); }; const id = window.setInterval(tick, 250); return () => window.clearInterval(id); }, []); - return ( -
+ return ( +
{liveSpeed > 0 ? formatSpeedMbps(liveSpeed) : "0 B/s"}
@@ -1783,9 +1781,10 @@ export function App(): ReactElement { const importQueueFocusHandlerRef = useRef<(() => void) | null>(null); const [contextMenu, setContextMenu] = useState(null); const ctxMenuRef = useRef(null); - const [linkPopup, setLinkPopup] = useState(null); - const [accountDialog, setAccountDialog] = useState(null); - const [accountDialogSearch, setAccountDialogSearch] = useState(""); + const [linkPopup, setLinkPopup] = useState(null); + const [accountDialog, setAccountDialog] = useState(null); + const [accountDialogSearch, setAccountDialogSearch] = useState(""); + const [accountDialogModeFilter, setAccountDialogModeFilter] = useState("all"); const [keyStatsPopup, setKeyStatsPopup] = useState(null); const [debridLinkHostLimits, setDebridLinkHostLimits] = useState>({}); const [debridLinkHostLimitsLoading, setDebridLinkHostLimitsLoading] = useState(false); @@ -2766,12 +2765,15 @@ export function App(): ReactElement { } return availableAccountOptions; }, [accountDialog, accountDialogOption, availableAccountOptions]); - const accountDialogSearchQuery = accountDialogSearch.trim().toLowerCase(); - const filteredAccountDialogOptions = useMemo(() => ( - accountDialogSelectableOptions.filter((option) => { - if (!accountDialogSearchQuery) { - return true; - } + const accountDialogSearchQuery = accountDialogSearch.trim().toLowerCase(); + const filteredAccountDialogOptions = useMemo(() => ( + accountDialogSelectableOptions.filter((option) => { + if (!matchesAccountModeFilter(option, accountDialogModeFilter)) { + return false; + } + if (!accountDialogSearchQuery) { + return true; + } const haystack = [ option.title, option.serviceLabel, @@ -2781,7 +2783,7 @@ export function App(): ReactElement { ].join(" ").toLowerCase(); return haystack.includes(accountDialogSearchQuery); }) - ), [accountDialogSearchQuery, accountDialogSelectableOptions]); + ), [accountDialogModeFilter, accountDialogSearchQuery, accountDialogSelectableOptions]); const accountTableStyle = useMemo(() => ({ "--account-col-service": `${accountColumnWidths.service}px`, "--account-col-mode": `${accountColumnWidths.mode}px`, @@ -3000,15 +3002,17 @@ export function App(): ReactElement { } }, [showToast]); - const openCreateAccountDialog = (): void => { - setAccountDialogSearch(""); - setAccountDialog(createAccountDialogState("create", null, settingsDraft)); - }; - - const openEditAccountDialog = (kind: AccountKind): void => { - setAccountDialogSearch(""); - setAccountDialog(createAccountDialogState("edit", kind, settingsDraft)); - }; + const openCreateAccountDialog = (): void => { + setAccountDialogSearch(""); + setAccountDialogModeFilter("all"); + setAccountDialog(createAccountDialogState("create", null, settingsDraft)); + }; + + const openEditAccountDialog = (kind: AccountKind): void => { + setAccountDialogSearch(""); + setAccountDialogModeFilter("all"); + setAccountDialog(createAccountDialogState("edit", kind, settingsDraft)); + }; const updateAccountDialogKind = (kind: AccountKind): void => { setAccountDialog((prev) => { @@ -3033,10 +3037,11 @@ export function App(): ReactElement { }); }; - const closeAccountDialog = useCallback((): void => { - setAccountDialog(null); - setAccountDialogSearch(""); - }, []); + const closeAccountDialog = useCallback((): void => { + setAccountDialog(null); + setAccountDialogSearch(""); + setAccountDialogModeFilter("all"); + }, []); const onSaveAccountDialog = async (quickAction?: AccountQuickAction): Promise => { if (!accountDialog) { @@ -3048,10 +3053,24 @@ export function App(): ReactElement { return; } const dialogSnapshot = accountDialog; - const selectedOption = dialogSnapshot.kind ? findAccountOption(dialogSnapshot.kind) : null; - await performQuickAction(async () => { - const nextDraft = applyAccountDialogToSettings(settingsDraft, dialogSnapshot); - await persistSpecificSettings(nextDraft); + const selectedOption = dialogSnapshot.kind ? findAccountOption(dialogSnapshot.kind) : null; + await performQuickAction(async () => { + const nextDraft = applyAccountDialogToSettings(settingsDraft, dialogSnapshot); + const requiresCredentialCheck = selectedOption?.kind === "megadebrid-api" + || selectedOption?.kind === "megadebrid-web" + || selectedOption?.kind === "debridlink-api"; + if (requiresCredentialCheck) { + const statuses = await window.rd.checkDebridAccounts(nextDraft); + const invalidStatuses = statuses.filter((status) => !status.valid); + if (invalidStatuses.length > 0) { + const details = invalidStatuses + .map((status) => status.message || "Zugangsdaten ungültig") + .join(" | "); + showToast(`Prüfung fehlgeschlagen: ${details}`, 4200); + return; + } + } + await persistSpecificSettings(nextDraft); closeAccountDialog(); if (quickAction) { await runAccountQuickAction(quickAction); @@ -3916,7 +3935,8 @@ export function App(): ReactElement { setContextMenu({ x, y, packageId, itemId }); }, []); - const speedHistoryRef = useRef<{ time: number; speed: number }[]>([]); + const speedHistoryRef = useRef<{ time: number; speed: number }[]>([]); + const speedSparklineStateRef = useRef({ history: [], display: 0 }); const dragSelectRef = useRef(false); const dragAnchorRef = useRef(null); const dragDidMoveRef = useRef(false); @@ -4998,14 +5018,14 @@ export function App(): ReactElement { -
- {tab === "downloads" && ( - - )} +
+
@@ -6300,12 +6320,24 @@ export function App(): ReactElement {
1. Account-Typ auswaehlen
- setAccountDialogSearch(event.target.value)} - /> +
+ + setAccountDialogSearch(event.target.value)} + /> +
Account @@ -6502,7 +6534,7 @@ export function App(): ReactElement { value={accountDialog.dailyLimitGb} onChange={(event) => setAccountDialog((prev) => prev ? { ...prev, dailyLimitGb: event.target.value } : prev)} /> -
Ab 00:00 wird der Zähler automatisch zurückgesetzt. Wenn das Limit erreicht ist, nutzt die App den nächsten Hoster aus der Reihenfolge.
+
Ab 00:00 wird der Zähler automatisch zurückgesetzt. Wenn das Limit erreicht ist, nutzt die App den nächsten Hoster aus der Reihenfolge.
{accountDialog.kind === "debridlink-api" && parseDebridLinkApiKeys(accountDialog.token).length > 0 && ( @@ -6597,9 +6629,9 @@ export function App(): ReactElement { Speichern + Login )} - +
)} diff --git a/src/renderer/account-ui.ts b/src/renderer/account-ui.ts new file mode 100644 index 0000000..598f52e --- /dev/null +++ b/src/renderer/account-ui.ts @@ -0,0 +1,15 @@ +export type AccountModeFilter = "all" | "api" | "web"; + +export interface AccountModeOption { + modeLabel: string; +} + +export function matchesAccountModeFilter(option: AccountModeOption, filter: AccountModeFilter): boolean { + if (filter === "all") { + return true; + } + if (filter === "api") { + return option.modeLabel === "API"; + } + return option.modeLabel.startsWith("Web"); +} diff --git a/src/renderer/download-speed-state.ts b/src/renderer/download-speed-state.ts new file mode 100644 index 0000000..83a764c --- /dev/null +++ b/src/renderer/download-speed-state.ts @@ -0,0 +1,24 @@ +export interface DownloadSpeedHistoryState { + history: number[]; + display: number; +} + +export const DOWNLOAD_SPEED_MAX_SAMPLES = 160; + +export function updateDownloadSpeedHistory( + state: DownloadSpeedHistoryState, + target: number, + maxSamples = DOWNLOAD_SPEED_MAX_SAMPLES +): DownloadSpeedHistoryState { + const safeTarget = Math.max(0, Number.isFinite(target) ? target : 0); + const alpha = safeTarget > state.display ? 0.45 : 0.12; + let display = state.display + (safeTarget - state.display) * alpha; + if (display < 1) { + display = 0; + } + const history = [...state.history, display]; + if (history.length > maxSamples) { + history.splice(0, history.length - maxSamples); + } + return { history, display }; +} diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 32d7dd9..14723c4 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -469,11 +469,22 @@ body, background: var(--button-bg-hover); } -.btn.accent { - background: linear-gradient(180deg, #f9a948, #e9881f); - color: #1c1206; - border-color: #f0982f; -} +.btn.accent { + background: linear-gradient(180deg, #f9a948, #e9881f); + color: #1c1206; + border-color: #f0982f; +} + +.btn.account-add-button { + background: linear-gradient(180deg, #63b3ed, #3182ce); + color: #061424; + border-color: #63b3ed; +} + +.btn.account-add-button:hover:not(:disabled) { + background: linear-gradient(180deg, #7cc4f4, #3b8ed8); + border-color: #90cdf4; +} .btn.danger { border-color: rgba(244, 63, 94, 0.7); @@ -536,7 +547,7 @@ body, z-index: 1; } -.speed-sparkline { +.speed-sparkline { display: flex; align-items: center; gap: 8px; @@ -544,8 +555,13 @@ body, padding: 0 10px; background: var(--field); border: 1px solid var(--border); - border-radius: 8px; -} + border-radius: 8px; +} + +.speed-sparkline-hidden { + visibility: hidden; + pointer-events: none; +} .speed-sparkline-canvas { width: 116px; @@ -1921,9 +1937,15 @@ body, padding-right: 4px; } -.account-picker-search { - width: 100%; -} +.account-picker-search { + width: 100%; +} + +.account-picker-toolbar { + display: grid; + grid-template-columns: minmax(145px, 0.32fr) minmax(0, 1fr); + gap: 8px; +} .account-modal label { display: block; @@ -2078,15 +2100,19 @@ body, gap: 12px; } -.account-modal-note { +.account-modal-note { padding: 12px 14px; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--accent) 26%, transparent); background: color-mix(in srgb, var(--accent) 8%, transparent); color: var(--text); font-size: 13px; - line-height: 1.5; -} + line-height: 1.5; +} + +.account-daily-limit-note { + margin-top: 10px; +} .account-mode-note { padding: 10px 12px; @@ -3227,11 +3253,15 @@ td { gap: 2px; } - .account-picker-list { - grid-template-columns: 1fr; - } - - .account-picker-head, + .account-picker-list { + grid-template-columns: 1fr; + } + + .account-picker-toolbar { + grid-template-columns: 1fr; + } + + .account-picker-head, .account-picker-row { grid-template-columns: 1fr; } diff --git a/src/shared/preload-api.ts b/src/shared/preload-api.ts index 4a90e91..0a4c75d 100644 --- a/src/shared/preload-api.ts +++ b/src/shared/preload-api.ts @@ -84,7 +84,7 @@ export interface ElectronApi { importBestDebridCookies: () => Promise; getAllDebridHostInfo: () => Promise; getDebridLinkHostLimits: () => Promise; - checkDebridAccounts: () => Promise; + checkDebridAccounts: (settings?: AppSettings) => Promise; checkMegaDebridAccount: (login: string, password: string) => Promise; retryExtraction: (packageId: string) => Promise; extractNow: (packageId: string) => Promise; diff --git a/tests/account-ui.test.ts b/tests/account-ui.test.ts new file mode 100644 index 0000000..b0a1f82 --- /dev/null +++ b/tests/account-ui.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { matchesAccountModeFilter } from "../src/renderer/account-ui"; + +describe("account mode filter", () => { + it("shows only API options for the API filter", () => { + expect(matchesAccountModeFilter({ modeLabel: "API" }, "api")).toBe(true); + expect(matchesAccountModeFilter({ modeLabel: "Web-Login" }, "api")).toBe(false); + }); + + it("shows Web-Login options for the Web filter", () => { + expect(matchesAccountModeFilter({ modeLabel: "Web-Login" }, "web")).toBe(true); + expect(matchesAccountModeFilter({ modeLabel: "API" }, "web")).toBe(false); + }); +}); diff --git a/tests/download-speed-state.test.ts b/tests/download-speed-state.test.ts new file mode 100644 index 0000000..f027add --- /dev/null +++ b/tests/download-speed-state.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { updateDownloadSpeedHistory } from "../src/renderer/download-speed-state"; + +describe("download speed history", () => { + it("keeps the smoothed display and history across successive updates", () => { + const first = updateDownloadSpeedHistory({ history: [], display: 0 }, 100, 10); + const second = updateDownloadSpeedHistory(first, 100, 10); + + expect(second.display).toBeGreaterThan(first.display); + expect(second.history).toHaveLength(2); + expect(second.history[0]).toBe(first.display); + }); + + it("keeps only the configured number of samples", () => { + let state = { history: [] as number[], display: 0 }; + for (let index = 0; index < 5; index += 1) { + state = updateDownloadSpeedHistory(state, index + 1, 3); + } + + expect(state.history).toHaveLength(3); + }); +});