Resolve complete child multipart selections as a single extraction set and report missing or incomplete local parts precisely. Stop manual, hybrid, and normal automatic extraction before invoking a backend when persisted completion no longer matches disk state, while retaining the explicit startup recovery path for intentionally cleaned sources. Keep never-started parent package statuses empty without hiding real retry or lifecycle activity, and stabilize the visual pointer test against its existing readiness budget.
302 lines
11 KiB
TypeScript
302 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { DownloadItem, PackageEntry } from "../src/shared/types";
|
|
import type { DownloadPackageRow } from "../src/renderer/views/downloads/downloads-model";
|
|
import { buildPackagePresentation } from "../src/renderer/views/downloads/package-presentation";
|
|
|
|
function item(id: string, fullStatus: string, overrides: Partial<DownloadItem> = {}): DownloadItem {
|
|
return {
|
|
id,
|
|
packageId: "pkg",
|
|
url: `https://example.invalid/${id}`,
|
|
provider: "realdebrid",
|
|
status: "completed",
|
|
retries: 0,
|
|
speedBps: 0,
|
|
downloadedBytes: 100,
|
|
totalBytes: 100,
|
|
progressPercent: 100,
|
|
fileName: `${id}.rar`,
|
|
targetPath: `C:\\Downloads\\${id}.rar`,
|
|
resumable: true,
|
|
attempts: 0,
|
|
lastError: "",
|
|
fullStatus,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function row(items: DownloadItem[], overrides: Partial<PackageEntry> = {}): DownloadPackageRow {
|
|
const entry = {
|
|
id: "pkg",
|
|
name: "Paket",
|
|
outputDir: "C:\\Downloads\\Paket",
|
|
extractDir: "C:\\Downloads\\_entpackt\\Paket",
|
|
itemIds: items.map((entry) => entry.id),
|
|
enabled: true,
|
|
cancelled: false,
|
|
status: "completed",
|
|
priority: "normal",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
...overrides
|
|
} as PackageEntry;
|
|
return { package: entry, items, allItems: items, collapsed: true };
|
|
}
|
|
|
|
describe("download package presentation", () => {
|
|
it("keeps the status empty for an over-package that has never started", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("queued", "Wartet", {
|
|
status: "queued",
|
|
attempts: 0,
|
|
retries: 0,
|
|
downloadedBytes: 0,
|
|
progressPercent: 0
|
|
})
|
|
], {
|
|
status: "queued",
|
|
downloadStartedAt: 0,
|
|
downloadCompletedAt: 0,
|
|
downloadEndedAt: 0,
|
|
postProcessQueuedAt: 0,
|
|
postProcessStartedAt: 0,
|
|
postProcessCompletedAt: 0,
|
|
terminalAt: 0
|
|
}));
|
|
|
|
expect(presentation.status).toBe("");
|
|
});
|
|
|
|
it("keeps the status empty for a disabled never-started over-package", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("queued", "Paket gestoppt", {
|
|
status: "queued",
|
|
attempts: 0,
|
|
retries: 0,
|
|
downloadedBytes: 0,
|
|
progressPercent: 0
|
|
})
|
|
], { status: "paused", enabled: false }));
|
|
|
|
expect(presentation.status).toBe("");
|
|
});
|
|
|
|
it("keeps retry activity visible when an older queued package lacks lifecycle timestamps", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("queued", "Link-Umwandlung erneut, Versuch 1/...", {
|
|
status: "queued",
|
|
attempts: 1,
|
|
retries: 1,
|
|
downloadedBytes: 0,
|
|
progressPercent: 0
|
|
})
|
|
], { status: "queued", downloadStartedAt: 0 }));
|
|
|
|
expect(presentation.status).toBe("Link-Umwandlung erneut");
|
|
});
|
|
|
|
it("keeps a queued status visible after the over-package has started once", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("queued", "Link-Umwandlung erneut, Versuch 1/...", {
|
|
status: "queued",
|
|
retries: 1,
|
|
downloadedBytes: 0,
|
|
progressPercent: 0
|
|
})
|
|
], { status: "queued", downloadStartedAt: 1 }));
|
|
|
|
expect(presentation.status).toBe("Link-Umwandlung erneut");
|
|
});
|
|
|
|
it("reserves 90 percent for completed downloads and 10 percent for extraction", () => {
|
|
expect(buildPackagePresentation(row([
|
|
item("a", "Entpack-Fehler: Passwort"),
|
|
item("b", "Entpack-Fehler: CRC")
|
|
])).progress.value).toBe(90);
|
|
|
|
expect(buildPackagePresentation(row([
|
|
item("a", "Entpackt in 4s"),
|
|
item("b", "Entpack-Fehler: CRC")
|
|
])).progress.value).toBe(95);
|
|
|
|
expect(buildPackagePresentation(row([
|
|
item("a", "Entpackt in 4s"),
|
|
item("b", "Entpackt in 5s")
|
|
])).progress.value).toBe(100);
|
|
});
|
|
|
|
it("keeps ordinary completed downloads at 100 percent when no extraction phase exists", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("a", "Fertig"),
|
|
item("b", "Fertig")
|
|
]));
|
|
expect(presentation.progress.value).toBe(100);
|
|
expect(presentation.status).toBe("Fertig");
|
|
});
|
|
|
|
it("does not move backwards when an archive download enters extraction", () => {
|
|
const active = item("archive", "Download läuft", {
|
|
status: "downloading",
|
|
downloadedBytes: 99,
|
|
progressPercent: 99
|
|
});
|
|
const before = buildPackagePresentation(row([active], { status: "downloading" }));
|
|
const after = buildPackagePresentation(row([{ ...active, status: "completed", downloadedBytes: 100, progressPercent: 100, fullStatus: "Entpacken - Ausstehend" }], { status: "extracting" }));
|
|
|
|
expect(before.progress.value).toBe(89);
|
|
expect(after.progress.value).toBe(90);
|
|
});
|
|
|
|
it("includes finalization progress in the reserved extraction range", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("archive", "Finalisieren - 99% · release.part01.rar")
|
|
], { status: "extracting", postProcessLabel: "Finalisieren - 99% (0/1) · release.part01.rar" }));
|
|
|
|
expect(presentation.progress.value).toBe(99);
|
|
expect(presentation.status).toBe("Finalisieren - 99% (0/1) · release.part01.rar");
|
|
});
|
|
|
|
it("shows the active CRC check instead of a completed fraction", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("archive", "CRC-Check läuft", { status: "integrity_check" })
|
|
], { status: "downloading" }));
|
|
|
|
expect(presentation.status).toBe("CRC-Check läuft");
|
|
});
|
|
|
|
it("keeps an active CRC check ahead of historical sibling extraction errors", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("failed", "Entpack-Fehler [old.part01.rar]: Checksum/CRC-Fehler im Archiv"),
|
|
item("active", "CRC-Check läuft", { status: "integrity_check" })
|
|
], { status: "downloading" }));
|
|
|
|
expect(presentation.status).toBe("CRC-Check läuft");
|
|
expect(presentation.details).toContain("1 Entpackfehler");
|
|
});
|
|
|
|
it.each([
|
|
["extracting", "Archive stabilisieren..."],
|
|
["extracting", "Entpacken vorbereiten..."],
|
|
["queued", "Entpacken wird neu gestartet..."],
|
|
["completed", "Nested Entpacken..."],
|
|
["completed", "Renaming..."],
|
|
["completed", "Tonspur..."],
|
|
["completed", "Aufräumen..."],
|
|
["completed", "Verschiebe Videos..."]
|
|
] as const)("keeps the active package phase %s / %s ahead of historical sibling errors", (packageStatus, postProcessLabel) => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("failed", "Entpack-Fehler [old.part01.rar]: Checksum/CRC-Fehler im Archiv"),
|
|
item("active", "Fertig")
|
|
], { status: packageStatus, postProcessLabel }));
|
|
|
|
expect(presentation.status).toBe(postProcessLabel);
|
|
expect(presentation.details).toContain("1 Entpackfehler");
|
|
expect(presentation.extractFailure?.id).toBe("failed");
|
|
});
|
|
|
|
it("keeps a running download ahead of historical sibling extraction errors", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("failed", "Entpack-Fehler [old.part01.rar]: Checksum/CRC-Fehler im Archiv"),
|
|
item("active", "Download läuft", { status: "downloading", downloadedBytes: 50, progressPercent: 50 })
|
|
], { status: "downloading" }));
|
|
|
|
expect(presentation.status).toBe("Download läuft");
|
|
expect(presentation.details).toContain("1 Entpackfehler");
|
|
});
|
|
|
|
it.each([
|
|
"Entpacken - Ausstehend",
|
|
"Entpacken - Warten auf Parts"
|
|
])("keeps a running download ahead of the sibling state %s", (fullStatus) => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("pending", fullStatus),
|
|
item("active", "Download läuft", { status: "downloading", downloadedBytes: 50, progressPercent: 50 })
|
|
], { status: "downloading" }));
|
|
|
|
expect(presentation.status).toBe("Download läuft");
|
|
});
|
|
|
|
it.each([
|
|
["queued", "Entpacken - Ausstehend", "Entpacken - Ausstehend"],
|
|
["extracting", "Entpacken - Ausstehend", "Entpacken - Ausstehend"],
|
|
["queued", "Entpacken - Warten auf Parts", "Entpacken - Warten auf Parts"],
|
|
["extracting", "Entpacken - Warten auf Parts", "Entpacken - Warten auf Parts"]
|
|
] as const)("keeps the pending extraction state %s / %s visible on the package", (packageStatus, fullStatus, expectedStatus) => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("archive", fullStatus)
|
|
], { status: packageStatus }));
|
|
|
|
expect(presentation.status).toBe(expectedStatus);
|
|
});
|
|
|
|
it.each([
|
|
"Entpacken 42% (1/1) · release.part01.rar",
|
|
"Passwort knacken: 50% (2/4)",
|
|
"Finalisieren - 99% (0/1) · release.part01.rar"
|
|
])("keeps the active extraction phase %s ahead of historical sibling errors", (postProcessLabel) => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("failed", "Entpack-Fehler [old.part01.rar]: Checksum/CRC-Fehler im Archiv"),
|
|
item("active", postProcessLabel)
|
|
], { status: "extracting", postProcessLabel }));
|
|
|
|
expect(presentation.status).toBe(postProcessLabel);
|
|
expect(presentation.details).toContain("1 Entpackfehler");
|
|
expect(presentation.extractFailure?.id).toBe("failed");
|
|
});
|
|
|
|
it("keeps the active disk wait ahead of historical sibling errors", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("failed", "Entpack-Fehler [old.part01.rar]: Checksum/CRC-Fehler im Archiv"),
|
|
item("active", "Warte auf Festplatte")
|
|
], { status: "queued" }));
|
|
|
|
expect(presentation.status).toBe("Warte auf Festplatte");
|
|
expect(presentation.details).toContain("1 Entpackfehler");
|
|
});
|
|
|
|
it("summarizes mixed extraction errors and a live retry instead of showing a fraction", () => {
|
|
const items = [
|
|
...Array.from({ length: 7 }, (_, index) => item(`failed-${index}`, "Entpack-Fehler: Keine entpackten Dateien erkannt")),
|
|
item("retry", "Link-Umwandlung erneut, Versuch 6/...", {
|
|
status: "validating",
|
|
retries: 6,
|
|
downloadedBytes: 0,
|
|
progressPercent: 0
|
|
})
|
|
];
|
|
const presentation = buildPackagePresentation(row(items, { status: "queued" }));
|
|
|
|
expect(presentation.status).toBe("7 Entpackfehler · Link-Umwandlung erneut");
|
|
expect(presentation.details).toContain("7 Entpackfehler");
|
|
expect(presentation.details).toContain("Link-Umwandlung erneut");
|
|
});
|
|
|
|
it("describes parallel link conversion retries without presenting the item count as attempts", () => {
|
|
const retries = Array.from({ length: 20 }, (_, index) => item(`retry-${index}`, "Link-Umwandlung erneut, Versuch 2/...", {
|
|
status: "validating",
|
|
retries: 2,
|
|
downloadedBytes: 0,
|
|
progressPercent: 0
|
|
}));
|
|
|
|
const presentation = buildPackagePresentation(row(retries, { status: "queued" }));
|
|
|
|
expect(presentation.status).toBe("Link-Umwandlung erneut");
|
|
expect(presentation.status).not.toContain("20");
|
|
});
|
|
|
|
it("keeps a single normal active download compact", () => {
|
|
const presentation = buildPackagePresentation(row([
|
|
item("active", "Download läuft", {
|
|
status: "downloading",
|
|
downloadedBytes: 50,
|
|
progressPercent: 50
|
|
})
|
|
], { status: "downloading" }));
|
|
|
|
expect(presentation.status).toBe("Download läuft");
|
|
});
|
|
});
|