Files
Multi-Debrid-Downloader/tests/visual/mock-electron-api.ts
T
Sucukdeluxe 55b8911e94 fix(downloads): restore actionable package states and extraction controls
Resolve child archive selections to complete multipart sets, support bulk package extraction, refresh extraction passwords at execution time, and keep archive failures scoped by their full paths.

Restore sortable download columns, preserve verified availability, classify package retry and extraction states, secure link copying through the preload bridge, and release cancelled provider work without stale cooldowns.
2026-08-22 22:52:49 +02:00

474 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 }),
inspectCollectorText: async () => clone(fixture.collector),
inspectCollectorContainers: async () => clone(fixture.collector),
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 (request) => {
for (const packageId of request.packageIds) {
const entry = fixture.snapshot.session.packages[packageId];
if (entry) {
entry.status = "extracting";
}
}
for (const itemId of request.itemIds) {
const item = fixture.snapshot.session.items[itemId];
const entry = item ? fixture.snapshot.session.packages[item.packageId] : undefined;
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
};
}