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.
335 lines
12 KiB
TypeScript
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'
|
|
);
|
|
});
|
|
});
|