Files
Multi-Debrid-Downloader/tests/visual/mock-electron-api.ts
T
Sucukdeluxe 2ea494cd5c Prepare v2.0.74 sidepanel reliability release
Restore and persist the package-based link collector with progressive metadata, bounded high-volume updates, safe hydration, visible selection, and complete localization.

Harden download controls, snapshot ordering, history pagination, statistics recovery, settings saves, backup imports, notification persistence, and Windows storage races with regression coverage.
2026-08-26 20:35:06 +02:00

471 lines
19 KiB
TypeScript

import type { ElectronApi } from "../../src/shared/preload-api";
import type { HistoryEntry, RendererSettings, RendererSettingsUpdate } from "../../src/shared/types";
import type { VisualFixture } from "./fixtures";
const stableNoopUnsubscribe = (): void => {};
function clone<T>(value: T): T {
return structuredClone(value);
}
export function createVisualElectronApi(
fixture: VisualFixture,
search = typeof window === "undefined" ? "" : window.location.search
): ElectronApi {
const searchParams = new URLSearchParams(search);
const historyState = searchParams.get("history-state");
if (searchParams.get("animations") === "off") fixture.snapshot.settings.animatePackageDisclosure = false;
let archivePasswordList = searchParams.get("archive-passwords") === "configured"
? "visual-archive-password-one\nvisual-archive-password-two"
: fixture.snapshot.settings.archivePasswordListConfigured ? "visual-archive-password" : "";
fixture.snapshot.settings.archivePasswordListConfigured = archivePasswordList.length > 0;
let historyRequestCount = 0;
const stateUpdateListeners = new Set<Parameters<ElectronApi["onStateUpdate"]>[0]>();
const emitStateUpdate = (): void => {
const snapshot = clone(fixture.snapshot);
for (const listener of stateUpdateListeners) listener(snapshot);
};
const updateSettings = (settings: RendererSettingsUpdate): RendererSettings => {
const { archivePasswordList: _archivePasswordList, notifyUrl: _notifyUrl, ...safe } = settings;
if (typeof _archivePasswordList === "string") {
archivePasswordList = _archivePasswordList;
fixture.snapshot.settings.archivePasswordListConfigured = archivePasswordList.trim().length > 0;
}
Object.assign(fixture.snapshot.settings, safe);
emitStateUpdate();
return clone(fixture.snapshot.settings);
};
return {
getSnapshot: async () => clone(fixture.snapshot),
getArchivePasswordList: async () => ({ passwords: archivePasswordList }),
getVersion: async () => "2.0.12",
checkUpdates: async () => clone(fixture.update),
installUpdate: async () => ({ started: true, message: "Visual update gestartet" }),
openExternal: async () => true,
updateSettings: async (settings) => updateSettings(settings),
resetProviderDailyUsage: async (provider) => {
fixture.snapshot.settings.providerDailyUsageBytes[provider] = 0;
return clone(fixture.snapshot.settings);
},
resetDebridLinkApiKeyDailyUsage: async (keyId) => {
fixture.snapshot.settings.debridLinkApiKeyDailyUsageBytes[keyId] = 0;
return clone(fixture.snapshot.settings);
},
createAccount: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
replaceAccount: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
updateAccountSecret: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
revealAccountSecret: async () => ({ secret: "" }),
deleteAccount: async () => ({ accountId: null, settings: clone(fixture.snapshot.settings), accounts: clone(fixture.snapshot.accounts) }),
addLinks: async () => ({ addedPackages: 0, addedLinks: 0, invalidCount: 0 }),
addContainers: async () => ({ addedPackages: 0, addedLinks: 0 }),
prepareCollectorText: async () => ({ packages: [], invalidCount: 0, duplicateCount: 0 }),
prepareCollectorContainers: async () => ({ packages: [], invalidCount: 0, duplicateCount: 0 }),
enrichCollectorPackages: async (request) => ({ packages: clone(request.packages), invalidCount: 0, duplicateCount: 0 }),
onCollectorEnrichmentProgress: () => () => {},
getCollectorState: async () => ({ packages: [], collapsedPackageIds: [] }),
getCollectorStateSync: () => ({ packages: [], collapsedPackageIds: [] }),
saveCollectorState: async (state) => clone(state),
saveCollectorStateSync: () => true,
getPathForDroppedFile: () => "",
getStartConflicts: async () => [],
resolveStartConflict: async (_packageId, policy) => ({
skipped: policy === "skip",
overwritten: policy === "overwrite"
}),
clearAll: async () => {
fixture.snapshot.session.packageOrder = [];
fixture.snapshot.session.packages = {};
fixture.snapshot.session.items = {};
fixture.snapshot.session.running = false;
fixture.snapshot.session.paused = false;
fixture.snapshot.session.totalDownloadedBytes = 0;
fixture.snapshot.packageSpeedBps = {};
fixture.snapshot.canStart = false;
fixture.snapshot.canStop = false;
fixture.snapshot.canPause = false;
},
start: async () => {
fixture.snapshot.session.running = true;
fixture.snapshot.session.paused = false;
fixture.snapshot.canStop = true;
fixture.snapshot.canPause = true;
},
startPackages: async (packageIds) => {
for (const packageId of packageIds) {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.status = "downloading";
}
}
fixture.snapshot.session.running = true;
},
stop: async () => {
fixture.snapshot.session.running = false;
fixture.snapshot.session.paused = false;
fixture.snapshot.canStop = false;
fixture.snapshot.canPause = false;
},
togglePause: async () => {
fixture.snapshot.session.paused = !fixture.snapshot.session.paused;
return fixture.snapshot.session.paused;
},
cancelPackage: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.cancelled = true;
entry.status = "cancelled";
}
},
renamePackage: async (packageId, newName) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.name = newName;
}
},
reorderPackages: async (packageIds) => {
fixture.snapshot.session.packageOrder = [...packageIds];
},
removeItem: async (itemId) => {
const item = fixture.snapshot.session.items[itemId];
if (item) {
const entry = fixture.snapshot.session.packages[item.packageId];
if (entry) {
entry.itemIds = entry.itemIds.filter((id) => id !== itemId);
}
delete fixture.snapshot.session.items[itemId];
}
},
togglePackage: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.enabled = !entry.enabled;
}
},
exportPackageSelection: async (packageIds) => ({
saved: true,
packageCount: packageIds.length,
linkCount: packageIds.reduce(
(count, packageId) => count + (fixture.snapshot.session.packages[packageId]?.itemIds.length ?? 0),
0
),
filePath: "C:\\Visual\\Exports\\packages.txt"
}),
exportItemSelection: async (itemIds) => ({
saved: true,
packageCount: new Set(
itemIds.map((itemId) => fixture.snapshot.session.items[itemId]?.packageId).filter(Boolean)
).size,
linkCount: itemIds.length,
filePath: "C:\\Visual\\Exports\\items.txt"
}),
exportQueue: async () => ({ saved: true }),
importQueue: async () => ({ addedPackages: 0, addedLinks: 0 }),
toggleClipboard: async () => {
fixture.snapshot.clipboardActive = !fixture.snapshot.clipboardActive;
fixture.snapshot.settings.clipboardWatch = fixture.snapshot.clipboardActive;
return fixture.snapshot.clipboardActive;
},
writeClipboardText: async () => true,
pickFolder: async () => "C:\\Visual\\Selected",
pickContainers: async () => ["C:\\Visual\\Containers\\visual.dlc"],
getSessionStats: async () => ({
bandwidth: {
samples: [
{ timestamp: 1786312680000, speedBps: 10485760 },
{ timestamp: 1786312740000, speedBps: 11534336 },
{ timestamp: 1786312800000, speedBps: 12582912 }
],
currentSpeedBps: 12582912,
averageSpeedBps: 11534336,
maxSpeedBps: 15728640,
totalBytesSession: 16642998272,
sessionDurationSeconds: 3600
},
totalDownloads: 4,
completedDownloads: 1,
failedDownloads: 1,
activeDownloads: 1,
queuedDownloads: 1
}),
resetSessionStats: async () => {
fixture.snapshot.stats.totalDownloaded = 0;
fixture.snapshot.stats.totalFilesSession = 0;
fixture.snapshot.session.totalDownloadedBytes = 0;
},
resetDownloadStats: async () => {
fixture.snapshot.stats.totalDownloadedAllTime = 0;
fixture.snapshot.stats.totalFilesAllTime = 0;
fixture.snapshot.settings.totalDownloadedAllTime = 0;
fixture.snapshot.settings.totalCompletedFilesAllTime = 0;
fixture.snapshot.settings.totalRuntimeAllTimeMs = 0;
},
restart: async () => {},
quit: async () => {},
exportBackup: async () => ({ saved: true }),
selectBackupImport: async () => ({ selected: true, requiresPassphrase: false }),
importBackup: async () => ({ restored: true, relaunch: false, message: "Visual backup importiert" }),
cancelBackupImport: async () => {},
exportOnlineBackup: async () => ({ key: "visual-online-backup-key" }),
importOnlineBackup: async () => ({ restored: true, relaunch: false, message: "Visual online backup importiert" }),
exportSupportBundle: async () => ({ saved: true, filePath: "C:\\Visual\\Support\\support.zip" }),
openLog: async () => {},
openLogDirectory: async () => {},
openAuditLog: async () => {},
openRenameLog: async () => {},
openSessionLog: async () => {},
openTraceLog: async () => {},
openPackageLog: async () => {},
openItemLog: async () => {},
getDebugSetupCheck: async () => ({
status: "ok",
enabled: false,
runtimeBaseDir: "C:\\Visual\\Runtime",
host: "127.0.0.1",
port: 7843,
localOnly: true,
tokenConfigured: true,
tokenPath: "C:\\Visual\\Runtime\\debug-token",
supportManifestPath: "C:\\Visual\\Runtime\\support-manifest.json",
supportManifestPresent: true,
traceConfigPath: "C:\\Visual\\Runtime\\trace-config.json",
traceLogPath: "C:\\Visual\\Runtime\\trace.log",
traceEnabled: fixture.traceConfig.enabled,
traceAutoDisableAt: fixture.traceConfig.autoDisableAt,
diskSpace: {
runtime: { path: "C:\\Visual\\Runtime", totalBytes: 1099511627776, freeBytes: 549755813888, freePercent: 50 },
output: { path: "C:\\Visual\\Downloads", totalBytes: 1099511627776, freeBytes: 549755813888, freePercent: 50 },
extract: { path: "C:\\Visual\\Extracted", totalBytes: 1099511627776, freeBytes: 549755813888, freePercent: 50 }
},
logSummary: {
totalBytes: 12288,
main: { path: "C:\\Visual\\Runtime\\main.log", exists: true, bytes: 4096 },
mainBackup: { path: null, exists: false, bytes: 0 },
audit: { path: "C:\\Visual\\Runtime\\audit.log", exists: true, bytes: 2048 },
auditBackup: { path: null, exists: false, bytes: 0 },
rename: { path: "C:\\Visual\\Runtime\\rename.log", exists: true, bytes: 1024 },
renameBackup: { path: null, exists: false, bytes: 0 },
session: { path: "C:\\Visual\\Runtime\\session.log", exists: true, bytes: 2048 },
trace: { path: "C:\\Visual\\Runtime\\trace.log", exists: true, bytes: 3072 },
traceBackup: { path: null, exists: false, bytes: 0 },
sessionLogs: { path: "C:\\Visual\\Runtime\\sessions", exists: true, fileCount: 2, bytes: 2048 },
packageLogs: { path: "C:\\Visual\\Runtime\\packages", exists: true, fileCount: 3, bytes: 3072 },
itemLogs: { path: "C:\\Visual\\Runtime\\items", exists: true, fileCount: 4, bytes: 4096 }
},
supportBundle: {
estimatedBytes: 24576,
estimatedEntries: 12,
duplicatedLiveLogBytes: 0,
note: "Visual support bundle"
},
warnings: [],
notes: ["Deterministischer Visual-Harness"],
localUrls: {
health: "http://127.0.0.1:7843/health",
meta: "http://127.0.0.1:7843/meta",
diagnostics: "http://127.0.0.1:7843/diagnostics"
},
remoteUrlTemplates: {
health: "https://visual.example.test/health",
meta: "https://visual.example.test/meta",
diagnostics: "https://visual.example.test/diagnostics"
}
}),
getRecentErrors: async () => [
{ ts: "2026-08-10T11:55:00.000Z", level: "WARN", message: "Visualer Beispielhinweis" }
],
testNotification: async () => true,
getTraceConfig: async () => clone(fixture.traceConfig),
setTraceEnabled: async (enabled) => {
fixture.traceConfig = { ...fixture.traceConfig, enabled };
return clone(fixture.traceConfig);
},
rotateDebugToken: async () => ({ path: "C:\\Visual\\Runtime\\debug-token" }),
getRemoteDiagnostics: async () => clone(fixture.remoteDiagnostics),
enableRemoteDiagnostics: async (input) => {
fixture.remoteDiagnostics = {
...fixture.remoteDiagnostics,
status: {
...fixture.remoteDiagnostics.status,
running: true,
host: input.hostMode === "local" ? "127.0.0.1" : "0.0.0.0",
port: input.port ?? 7843,
localOnly: input.hostMode === "local",
allowlistCount: input.allowlist.length
},
publicHost: input.publicHost,
name: input.name ?? fixture.remoteDiagnostics.name,
allowlist: [...input.allowlist]
};
return clone(fixture.remoteDiagnostics);
},
disableRemoteDiagnostics: async () => {
fixture.remoteDiagnostics = {
...fixture.remoteDiagnostics,
status: { ...fixture.remoteDiagnostics.status, running: false }
};
return clone(fixture.remoteDiagnostics);
},
rotateRemoteDiagnosticsToken: async () => clone(fixture.remoteDiagnostics),
openRealDebridLogin: async () => {},
openAllDebridLogin: async () => {},
importBestDebridCookies: async () => 2,
getAllDebridHostInfo: async () => ({
host: "rapidgator.net",
source: "api",
state: "up",
statusLabel: "Verfügbar",
fetchedAt: 1786312800000,
lastCheckedAt: 1786312740000,
quota: 42,
quotaMax: 100,
quotaType: "daily",
limitSimuDl: 8,
note: "Visual host status"
}),
getDebridLinkHostLimits: async () => {
const primaryKey = fixture.snapshot.accounts.find((account) => account.kind === "debridlink-api");
if (!primaryKey) return [];
return [{
keyId: primaryKey.accountId,
keyLabel: "Key 1",
host: "ddownload.com",
fetchedAt: 1786312800000,
trafficCurrentBytes: 53687091200,
trafficMaxBytes: 268435456000,
linksCurrent: 12,
linksMax: 100,
note: "Visual quota",
state: "ready",
stateLabel: "Bereit",
stateDetail: "Kontingent verfügbar",
cooldownUntil: null,
cooldownRemainingMs: 0,
lastCheckedAt: 1786312740000,
hostState: "up",
hostStateLabel: "Online",
hostNote: "Hoster verfügbar"
}];
},
checkDebridAccounts: async () => clone(Object.values(fixture.snapshot.settings.debridAccountStatuses)),
checkAccountCredentials: async (input) => clone(fixture.snapshot.accounts.find((account) => account.accountId === input.accountId)?.status || {
accountId: input.accountId || "visual-account",
provider: input.kind === "debridlink-api" ? "debridlink" : "megadebrid",
label: "Visual Account",
maskedLogin: "vi***al",
valid: true,
isPremium: true,
premiumUntilMs: null,
message: "Premium aktiv",
checkedAt: 1786312800000
}),
retryExtraction: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.status = "extracting";
}
},
extractNow: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.status = "extracting";
}
},
resetPackage: async (packageId) => {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.status = "queued";
entry.cancelled = false;
}
},
getHistory: async () => {
historyRequestCount += 1;
if (historyRequestCount > 1 && historyState === "loading") {
return new Promise<HistoryEntry[]>(() => {});
}
if (historyRequestCount > 1 && historyState === "error") {
throw new Error("Visual history load failed");
}
return clone(fixture.history);
},
clearHistory: async () => {
fixture.history.splice(0, fixture.history.length);
},
removeHistoryEntry: async (entryId) => {
const index = fixture.history.findIndex((entry) => entry.id === entryId);
if (index >= 0) {
fixture.history.splice(index, 1);
}
},
removeHistoryEntries: async (entryIds) => {
const removed = new Set(entryIds);
const retained = fixture.history.filter((entry) => !removed.has(entry.id));
fixture.history.splice(0, fixture.history.length, ...retained);
},
revealHistoryEntry: async (entryId) => fixture.history.some((entry) => entry.id === entryId)
? { ok: true }
: { ok: false, reason: "entry-not-found" },
setPackagePriority: async (packageId, priority) => {
const entry = fixture.snapshot.session.packages[packageId];
if (!entry || entry.priority === priority) return;
entry.priority = priority;
const order = fixture.snapshot.session.packageOrder;
const currentIndex = order.indexOf(packageId);
if (currentIndex >= 0) {
order.splice(currentIndex, 1);
const priorityRank = { high: 0, normal: 1, low: 2 } as const;
let insertAt = order.length;
for (let index = 0; index < order.length; index += 1) {
const otherPriority = fixture.snapshot.session.packages[order[index]]?.priority || "normal";
if (priorityRank[otherPriority] > priorityRank[priority]) {
insertAt = index;
break;
}
}
order.splice(insertAt, 0, packageId);
}
emitStateUpdate();
},
skipItems: async (itemIds) => {
for (const itemId of itemIds) {
const item = fixture.snapshot.session.items[itemId];
if (item) {
item.status = "cancelled";
item.fullStatus = "Übersprungen";
}
}
},
resetItems: async (itemIds) => {
for (const itemId of itemIds) {
const item = fixture.snapshot.session.items[itemId];
if (item) {
item.status = "queued";
item.downloadedBytes = 0;
item.progressPercent = 0;
item.speedBps = 0;
item.lastError = "";
item.fullStatus = "In Warteschlange";
}
}
},
startItems: async (itemIds) => {
for (const itemId of itemIds) {
const item = fixture.snapshot.session.items[itemId];
if (item) {
item.status = "downloading";
item.fullStatus = "Download läuft";
}
}
fixture.snapshot.session.running = true;
},
reportRendererError: () => {},
onStateUpdate: (callback) => {
stateUpdateListeners.add(callback);
return () => stateUpdateListeners.delete(callback);
},
onHistoryEntryAdded: () => stableNoopUnsubscribe,
onClipboardDetected: () => stableNoopUnsubscribe,
onUpdateInstallProgress: () => stableNoopUnsubscribe
};
}