Files
Multi-Debrid-Downloader/tests/visual-fixtures.test.ts
Sucukdeluxe 1b7caba2eb feat: add Deepbrid, daily scheduling, and notification center
Add encrypted Deepbrid API accounts with account validation, provider routing, fallback, usage tracking, safe error handling, and verified 1Fichier downloads. Restore persistent recurring daily starts with local-calendar deduplication and legacy schedule compatibility. Add durable Discord package, run, remaining-volume, stall, and recovery notifications with privacy-safe telemetry and disk-failure recovery.
2026-08-24 07:37:55 +02:00

335 lines
12 KiB
TypeScript

import React, { type ReactElement } from "react";
import { describe, expect, it } from "vitest";
import { App } from "../src/renderer/App";
import type { ElectronApi } from "../src/shared/preload-api";
import * as visualFixtures from "./visual/fixtures";
import * as visualMain from "./visual/main";
import { createVisualElectronApi } from "./visual/mock-electron-api";
const { createVisualFixture } = visualFixtures;
interface TestVisualRoot {
innerText: string;
textContent: string | null;
dataset: {
visualError?: string;
};
}
interface TestVisualMarker {
visualReady?: string;
visualScenario?: string;
}
function createTestVisualBootstrap(
search: string,
initialInnerText: string,
onFrame: (frame: number, rootElement: TestVisualRoot) => void,
maxFrames = 2
) {
const rootElement: TestVisualRoot = {
innerText: initialInnerText,
textContent: initialInnerText,
dataset: {}
};
const marker: TestVisualMarker = {};
const createdRootElements: TestVisualRoot[] = [];
const renderedElements: ReactElement[] = [];
const assignedApis: ElectronApi[] = [];
const markerValuesByFrame: Array<string | undefined> = [];
const clockInstalls: string[] = [];
let frameCount = 0;
const runtime = {
search,
rootElement,
marker,
maxFrames,
installClock(): void {
clockInstalls.push(search);
},
setElectronApi(api: ElectronApi): void {
assignedApis.push(api);
},
createRoot(element: TestVisualRoot) {
createdRootElements.push(element);
return {
render(renderedElement: ReactElement): void {
renderedElements.push(renderedElement);
}
};
},
requestFrame(callback: FrameRequestCallback): number {
frameCount += 1;
markerValuesByFrame.push(marker.visualReady);
onFrame(frameCount, rootElement);
callback(0);
return frameCount;
}
};
return {
runtime,
rootElement,
marker,
createdRootElements,
renderedElements,
assignedApis,
markerValuesByFrame,
clockInstalls
};
}
describe("visual fixtures", () => {
it("keeps empty, dense and update states deterministic and distinct", () => {
const empty = createVisualFixture("empty");
const dense = createVisualFixture("dense");
const update = createVisualFixture("update");
expect(Object.keys(empty.snapshot.session.packages)).toHaveLength(0);
expect(Object.keys(dense.snapshot.session.packages).length).toBeGreaterThan(1);
expect(dense.snapshot.stats.rolling24Hours?.accounts).toHaveLength(2);
expect(dense.snapshot.stats.statistics?.minutes).toEqual([]);
expect(update.update.latestTag).toBe("v9.9.9");
expect(createVisualFixture("dense")).toEqual(dense);
});
it("keeps the complete notification center state deterministic without exposing its webhook", () => {
const settings = createVisualFixture("dense").snapshot.settings;
expect(settings).toEqual(expect.objectContaining({
notifyUrlConfigured: true,
notifyMention: "@visual",
notifyOnPackageCompleted: true,
notifyOnPackageFailed: true,
notifyPackageSuccessMode: "digest",
notifyOnRunFinished: true,
notifyOnRemainingBelow: true,
notifyRemainingThresholdGb: 75,
notifyOnDownloadStall: true,
notifyStallAfterSeconds: 120,
notifyStallCooldownMinutes: 15,
notifyOnDownloadRecovery: true
}));
expect(settings).not.toHaveProperty("notifyUrl");
});
it("freezes runtime and recurring chart timers across visual frames", async () => {
const dense = createVisualFixture("dense");
const originalDateNow = Date.now;
let timerTicks = 0;
const timerTarget = {
setInterval(handler: TimerHandler, _timeout?: number): number {
if (typeof handler === "function") {
handler();
}
return 1;
}
};
const restore = visualFixtures.installVisualClock(timerTarget);
try {
const runtime = (): number => dense.snapshot.stats.sessionRuntimeMs
+ Math.max(0, Date.now() - dense.snapshot.stats.runtimeMeasuredAt);
const firstRuntime = runtime();
const frameTimes: number[] = [];
timerTarget.setInterval(() => { timerTicks += 1; }, 250);
await visualFixtures.waitForVisualFrames((callback) => {
frameTimes.push(Date.now());
callback(0);
return frameTimes.length;
});
timerTarget.setInterval(() => { timerTicks += 1; }, 250);
timerTarget.setInterval(() => { timerTicks += 1; }, 1000);
expect(Date.now()).toBe(1786312800000);
expect(firstRuntime).toBe(3600000);
expect(runtime()).toBe(firstRuntime);
expect(frameTimes).toEqual([1786312800000, 1786312800000]);
expect(timerTicks).toBe(0);
} finally {
restore();
}
expect(Date.now).toBe(originalDateNow);
});
it("aligns dense account table values with credential-derived account IDs", async () => {
const dense = createVisualFixture("dense");
const settings = dense.snapshot.settings;
const megaAccount = dense.snapshot.accounts.find((account) => account.kind === "megadebrid-api");
const debridLinkKeys = dense.snapshot.accounts.filter((account) => account.kind === "debridlink-api");
const megaAccountId = megaAccount?.accountId || "";
expect(megaAccountId).toBe("mda_2f92guyzhdf6j");
expect(debridLinkKeys.map((entry) => entry.accountId)).toEqual([
"dlk_1ix5qlyx6mtm1",
"dlk_1ix5pfvlg4nkg"
]);
expect(settings.debridAccountStatuses[megaAccountId]?.valid).toBe(true);
expect(settings.megaDebridAccountDailyLimitBytes[megaAccountId]).toBeGreaterThan(0);
expect(settings.megaDebridAccountDailyUsageBytes[megaAccountId]).toBeGreaterThan(0);
expect(settings.megaDebridAccountTotalUsageBytes[megaAccountId]).toBeGreaterThan(
settings.megaDebridAccountDailyUsageBytes[megaAccountId]
);
for (const entry of debridLinkKeys) {
expect(settings.debridAccountStatuses[entry.accountId]?.valid).toBe(true);
expect(settings.debridLinkApiKeyDailyLimitBytes[entry.accountId]).toBeGreaterThan(0);
expect(settings.debridLinkApiKeyDailyUsageBytes[entry.accountId]).toBeGreaterThan(0);
expect(settings.debridLinkApiKeyDailyUsageBytes[entry.accountId]).toBeLessThan(
settings.debridLinkApiKeyDailyLimitBytes[entry.accountId]
);
expect(settings.debridLinkApiKeyTotalUsageBytes[entry.accountId]).toBeGreaterThan(
settings.debridLinkApiKeyDailyUsageBytes[entry.accountId]
);
}
const debridLinkItem = Object.values(dense.snapshot.session.items).find(
(item) => item.provider === "debridlink"
);
expect(debridLinkItem?.providerAccountId).toBe(debridLinkKeys[0].accountId);
const hostLimits = await createVisualElectronApi(dense).getDebridLinkHostLimits();
expect(hostLimits[0]?.keyId).toBe(debridLinkKeys[0].accountId);
});
it("includes the configured synthetic Deepbrid account", () => {
const dense = createVisualFixture("dense");
expect(dense.snapshot.accounts).toEqual(expect.arrayContaining([
expect.objectContaining({ accountId: "svc-deepbrid", kind: "deepbrid-api", provider: "deepbrid" })
]));
});
it("stores every mutable bridge state inside the visual fixture", async () => {
const dense = createVisualFixture("dense");
const api = createVisualElectronApi(dense);
await api.setTraceEnabled(true);
expect(dense).toHaveProperty("traceConfig.enabled", true);
await api.enableRemoteDiagnostics({
hostMode: "network",
publicHost: "capture.example.test",
port: 8123,
allowlist: ["192.0.2.10"],
name: "Capture Harness"
});
expect(dense).toHaveProperty("remoteDiagnostics.status.running", true);
expect(dense).toHaveProperty("remoteDiagnostics.status.port", 8123);
expect(dense).toHaveProperty("remoteDiagnostics.publicHost", "capture.example.test");
await api.disableRemoteDiagnostics();
expect(dense).toHaveProperty("remoteDiagnostics.status.running", false);
});
it("emits the priority-sorted snapshot so visual tests can observe package motion", async () => {
const dense = createVisualFixture("dense");
const api = createVisualElectronApi(dense);
const snapshots: string[][] = [];
const unsubscribe = api.onStateUpdate((snapshot) => snapshots.push([...snapshot.session.packageOrder]));
const target = dense.snapshot.session.packageOrder[0];
await api.setPackagePriority(target, "low");
expect(dense.snapshot.session.packageOrder.at(-1)).toBe(target);
expect(snapshots.at(-1)?.at(-1)).toBe(target);
unsubscribe();
});
it("can boot the isolated renderer with animations disabled", async () => {
const dense = createVisualFixture("dense");
const api = createVisualElectronApi(dense, "?animations=off");
expect((await api.getSnapshot()).settings.animatePackageDisclosure).toBe(false);
});
it("keeps a configured archive password list stateful in the isolated renderer", async () => {
const dense = createVisualFixture("dense");
const api = createVisualElectronApi(dense, "?archive-passwords=configured");
expect(await api.getArchivePasswordList()).toEqual({
passwords: "visual-archive-password-one\nvisual-archive-password-two"
});
expect((await api.getSnapshot()).settings.archivePasswordListConfigured).toBe(true);
await api.updateSettings({ archivePasswordList: "updated-visual-password" });
expect(await api.getArchivePasswordList()).toEqual({ passwords: "updated-visual-password" });
});
it("boots the dense query once and waits for both visible package names", async () => {
expect(typeof window).toBe("undefined");
const harness = createTestVisualBootstrap(
"?scenario=dense",
"Dokumentation Staffel 1",
(frame, rootElement) => {
if (frame === 3) {
rootElement.innerText = "Dokumentation Staffel 1 Konzertmitschnitt 2026";
}
}
);
await visualMain.startVisualHarness(harness.runtime);
expect(harness.clockInstalls).toHaveLength(1);
expect(harness.createdRootElements).toEqual([harness.rootElement]);
expect(harness.renderedElements).toHaveLength(1);
expect(harness.renderedElements[0].type).toBe(App);
expect(harness.renderedElements[0].type).not.toBe(React.StrictMode);
expect(harness.assignedApis).toHaveLength(1);
const snapshot = await harness.assignedApis[0].getSnapshot();
expect(Object.values(snapshot.session.packages).map((pkg) => pkg.name)).toEqual([
"Dokumentation Staffel 1",
"Konzertmitschnitt 2026",
"Archiv mit Wiederholung"
]);
expect(harness.marker.visualScenario).toBe("dense");
expect(harness.markerValuesByFrame).toEqual([undefined, undefined, undefined]);
expect(harness.marker.visualReady).toBe("true");
expect(harness.rootElement.dataset.visualError).toBeUndefined();
expect(typeof window).toBe("undefined");
});
it("resolves the update query and waits for visible v9.9.9 evidence", async () => {
const harness = createTestVisualBootstrap(
"?scenario=update",
"Update verfügbar",
(frame, rootElement) => {
if (frame === 3) {
rootElement.innerText = "Update verfügbar v9.9.9";
}
}
);
await visualMain.startVisualHarness(harness.runtime);
expect(harness.createdRootElements).toHaveLength(1);
expect(harness.renderedElements).toHaveLength(1);
expect(harness.marker.visualScenario).toBe("update");
expect(harness.markerValuesByFrame).toEqual([undefined, undefined, undefined]);
expect(harness.marker.visualReady).toBe("true");
expect((await harness.assignedApis[0].checkUpdates()).latestTag).toBe("v9.9.9");
});
it("catches missing update evidence inside the bootstrap and exposes a local error", async () => {
const harness = createTestVisualBootstrap(
"?scenario=update",
"Update verfügbar",
() => undefined,
0
);
harness.marker.visualReady = "true";
await expect(visualMain.startVisualHarness(harness.runtime)).resolves.toBeUndefined();
expect(harness.createdRootElements).toHaveLength(1);
expect(harness.renderedElements).toHaveLength(1);
expect(harness.marker.visualScenario).toBe("update");
expect(harness.markerValuesByFrame).toEqual([undefined, undefined]);
expect(harness.marker.visualReady).toBeUndefined();
expect(harness.rootElement.dataset.visualError).toBe("true");
expect(harness.rootElement.textContent).toBe(
'Visual-Harness-Fehler: Visual-Harness-Szenario "update" ist nicht bereit: v9.9.9 fehlt'
);
});
});