Stream completed metadata checks into the collector while preserving generation-based stale-result protection. Derive row statuses from verified availability, move analysis feedback into the sidebar, center table values under their headers, and keep the header synchronized during horizontal scrolling.
561 lines
27 KiB
TypeScript
561 lines
27 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildCollectorTransferPackages,
|
|
buildCollectorWorkspaceViewModel,
|
|
mergeCollectorEnrichment,
|
|
mergeCollectorPackages,
|
|
removeCollectorLinks,
|
|
selectCollectorPackageLinks,
|
|
type CollectorPackage
|
|
} from "../src/renderer/views/collector/collector-model";
|
|
import {
|
|
CollectorContent,
|
|
CollectorInputDialog,
|
|
MemoizedCollectorContent,
|
|
CollectorSidebar,
|
|
CollectorSidebarStatus,
|
|
CollectorToolbar,
|
|
CollectorView,
|
|
collectorFileInteractionAttributes,
|
|
collectorHeaderScrollStyle,
|
|
collectorPackageIntrinsicBlockSize,
|
|
toggleAllCollectorPackageIds,
|
|
type CollectorViewActions
|
|
} from "../src/renderer/views/collector/CollectorView";
|
|
import {
|
|
getCollectorDisclosurePinnedIds,
|
|
getCollectorDisclosureViewportIds,
|
|
getCollectorFocusPackageId,
|
|
mergeCollectorPinnedIds,
|
|
resolveCollectorTransitionPins
|
|
} from "../src/renderer/views/collector/collector-disclosure";
|
|
|
|
function visitElements(node: ReactNode, visit: (element: ReactElement) => void): void {
|
|
if (Array.isArray(node)) {
|
|
node.forEach((child) => visitElements(child, visit));
|
|
return;
|
|
}
|
|
if (!isValidElement(node)) return;
|
|
visit(node);
|
|
visitElements(node.props.children, visit);
|
|
visitElements(node.props.actions, visit);
|
|
}
|
|
|
|
function findElement(node: ReactNode, predicate: (element: ReactElement) => boolean): ReactElement {
|
|
let result: ReactElement | null = null;
|
|
visitElements(node, (element) => {
|
|
if (!result && predicate(element)) result = element;
|
|
});
|
|
if (!result) throw new Error("Element not found");
|
|
return result;
|
|
}
|
|
|
|
function findButton(node: ReactNode, label: string): ReactElement {
|
|
return findElement(node, (element) => element.type === "button" && element.props.children === label);
|
|
}
|
|
|
|
function createActions(overrides: Partial<CollectorViewActions> = {}): CollectorViewActions {
|
|
return {
|
|
onFilterChange: () => {},
|
|
onOpenInput: () => {},
|
|
onImportDlc: () => {},
|
|
onImportFile: () => {},
|
|
onSubmitSelected: () => {},
|
|
onSubmitAll: () => {},
|
|
onQueryChange: () => {},
|
|
onLinkSelectionChange: () => {},
|
|
onPackageSelectionChange: () => {},
|
|
onPackageCollapseChange: () => {},
|
|
onToggleAllPackages: () => {},
|
|
onRemoveSelected: () => {},
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
const packages: CollectorPackage[] = [{
|
|
id: "package-sbs",
|
|
name: "SBS14HD",
|
|
nameSource: "inferred",
|
|
addedAt: 1_000,
|
|
links: [
|
|
{ id: "link-1", url: "https://1fichier.com/?one11111", fileName: "SBS14HD.part01.rar", fileSizeBytes: 471_859_200, hoster: "1fichier", availability: "online", status: "ready", addedAt: 1_000 },
|
|
{ id: "link-2", url: "https://1fichier.com/?two22222", fileName: "SBS14HD.part02.rar", fileSizeBytes: 471_859_200, hoster: "1fichier", availability: "online", status: "ready", addedAt: 1_000 }
|
|
]
|
|
}, {
|
|
id: "package-mixed",
|
|
name: "Mixed",
|
|
nameSource: "inferred",
|
|
addedAt: 2_000,
|
|
links: [
|
|
{ id: "link-3", url: "https://example.test/unknown", fileName: "download.bin", fileSizeBytes: null, hoster: "example", availability: "unknown", status: "unknown", addedAt: 2_000 },
|
|
{ id: "link-4", url: "https://example.test/offline", fileName: "offline.bin", fileSizeBytes: null, hoster: "example", availability: "offline", status: "offline", addedAt: 2_000 }
|
|
]
|
|
}];
|
|
|
|
describe("collector workspace model", () => {
|
|
it("merges late enrichment by URL without duplicates and moves the link into its resolved package", () => {
|
|
const initial: CollectorPackage[] = [{
|
|
id: "pending",
|
|
name: "Unsortiert",
|
|
nameSource: "inferred",
|
|
addedAt: 1_000,
|
|
links: [{ id: "stable-link", url: "https://1fichier.com/?one11111", fileName: "download.bin", fileSizeBytes: null, hoster: "1fichier", availability: "unknown", status: "unknown", addedAt: 1_000 }]
|
|
}];
|
|
const enriched: CollectorPackage[] = [{
|
|
id: "resolved",
|
|
name: "SBS14HD",
|
|
nameSource: "inferred",
|
|
addedAt: 2_000,
|
|
links: [{ id: "replacement-id", url: "https://1fichier.com/?one11111", fileName: "SBS14HD.part01.rar", fileSizeBytes: 471_859_200, hoster: "1fichier", availability: "online", status: "ready", addedAt: 2_000 }]
|
|
}];
|
|
|
|
const result = mergeCollectorPackages(initial, enriched);
|
|
|
|
expect(result).toEqual(expect.objectContaining({ addedLinks: 0, duplicateLinks: 0, enrichedLinks: 1 }));
|
|
expect(result.packages).toHaveLength(1);
|
|
expect(result.packages[0].name).toBe("SBS14HD");
|
|
expect(result.packages[0].links).toEqual([expect.objectContaining({
|
|
id: "stable-link",
|
|
fileName: "SBS14HD.part01.rar",
|
|
fileSizeBytes: 471_859_200,
|
|
availability: "online",
|
|
status: "ready",
|
|
addedAt: 1_000
|
|
})]);
|
|
});
|
|
|
|
it("deduplicates repeated incoming URLs while preserving distinct links", () => {
|
|
const incoming: CollectorPackage[] = [{
|
|
id: "incoming",
|
|
name: "SBS14HD",
|
|
nameSource: "inferred",
|
|
addedAt: 3_000,
|
|
links: [
|
|
{ ...packages[0].links[0], id: "duplicate" },
|
|
{ ...packages[0].links[0], id: "duplicate-again" },
|
|
{ id: "link-5", url: "https://1fichier.com/?three333", fileName: "SBS14HD.part03.rar", fileSizeBytes: 10, hoster: "1fichier", availability: "online", status: "ready", addedAt: 3_000 }
|
|
]
|
|
}];
|
|
const result = mergeCollectorPackages(packages, incoming);
|
|
|
|
expect(result.addedLinks).toBe(1);
|
|
expect(result.duplicateLinks).toBe(2);
|
|
expect(result.packages[0].links.map((link) => link.id)).toEqual(["link-1", "link-2", "link-5"]);
|
|
});
|
|
|
|
it("does not restore links removed while background enrichment is running", () => {
|
|
const current = [{ ...packages[0], links: [packages[0].links[0]] }];
|
|
const incoming = [{ ...packages[0], links: packages[0].links.map((link) => ({ ...link, status: "ready" as const })) }];
|
|
|
|
expect(mergeCollectorEnrichment(current, incoming).packages[0].links.map((link) => link.id)).toEqual(["link-1"]);
|
|
expect(mergeCollectorEnrichment([], incoming).packages).toEqual([]);
|
|
});
|
|
|
|
it("does not replace known metadata with a repeated unknown skeleton", () => {
|
|
const current = [{ ...packages[0], links: [packages[0].links[0]] }];
|
|
const incoming: CollectorPackage[] = [{
|
|
id: "pending",
|
|
name: "1Fichier",
|
|
nameSource: "inferred",
|
|
addedAt: 4_000,
|
|
links: [{
|
|
...packages[0].links[0],
|
|
id: "replacement",
|
|
fileName: "download.bin",
|
|
fileSizeBytes: null,
|
|
availability: "unknown",
|
|
status: "unknown",
|
|
addedAt: 4_000
|
|
}]
|
|
}];
|
|
|
|
const result = mergeCollectorPackages(current, incoming);
|
|
|
|
expect(result.packages).toHaveLength(1);
|
|
expect(result.packages[0].name).toBe("SBS14HD");
|
|
expect(result.packages[0].links[0]).toEqual(expect.objectContaining({
|
|
id: "link-1",
|
|
fileName: "SBS14HD.part01.rar",
|
|
fileSizeBytes: 471_859_200,
|
|
availability: "online",
|
|
status: "ready",
|
|
addedAt: 1_000
|
|
}));
|
|
});
|
|
|
|
it("applies a resolved package name even when link metadata was already complete", () => {
|
|
const current: CollectorPackage[] = [{
|
|
id: "pending",
|
|
name: "Unsortiert",
|
|
nameSource: "inferred",
|
|
addedAt: 1_000,
|
|
links: [{ ...packages[0].links[0], id: "stable-link" }]
|
|
}];
|
|
const incoming: CollectorPackage[] = [{
|
|
id: "resolved",
|
|
name: "SBS14HD",
|
|
nameSource: "inferred",
|
|
addedAt: 2_000,
|
|
links: [{ ...packages[0].links[0], id: "replacement-link" }]
|
|
}];
|
|
|
|
const result = mergeCollectorPackages(current, incoming);
|
|
|
|
expect(result.enrichedLinks).toBe(1);
|
|
expect(result.duplicateLinks).toBe(0);
|
|
expect(result.packages.map((pkg) => [pkg.name, pkg.links.map((link) => link.id)])).toEqual([
|
|
["SBS14HD", ["stable-link"]]
|
|
]);
|
|
});
|
|
|
|
it("keeps explicit package names authoritative over inferred enrichment", () => {
|
|
const current: CollectorPackage[] = [{
|
|
id: "explicit",
|
|
name: "Meine Staffel",
|
|
nameSource: "explicit",
|
|
addedAt: 1_000,
|
|
links: [{ ...packages[0].links[0], id: "stable-link", availability: "unknown", status: "unknown" }]
|
|
}];
|
|
const incoming: CollectorPackage[] = [{
|
|
id: "inferred",
|
|
name: "SBS14HD",
|
|
nameSource: "inferred",
|
|
addedAt: 2_000,
|
|
links: [{ ...packages[0].links[0], id: "replacement-link" }]
|
|
}];
|
|
|
|
const result = mergeCollectorPackages(current, incoming);
|
|
|
|
expect(result.packages).toHaveLength(1);
|
|
expect(result.packages[0]).toEqual(expect.objectContaining({ name: "Meine Staffel", nameSource: "explicit" }));
|
|
expect(result.packages[0].links[0]).toEqual(expect.objectContaining({ id: "stable-link", availability: "online", status: "ready" }));
|
|
});
|
|
|
|
it("upgrades an inferred package identity when explicit metadata arrives", () => {
|
|
const current: CollectorPackage[] = [{
|
|
id: "inferred",
|
|
name: "SBS14HD",
|
|
nameSource: "inferred",
|
|
addedAt: 1_000,
|
|
links: [{ ...packages[0].links[0], id: "stable-link", availability: "unknown", status: "unknown" }]
|
|
}];
|
|
const incoming: CollectorPackage[] = [{
|
|
id: "explicit",
|
|
name: "SBS14HD",
|
|
nameSource: "explicit",
|
|
addedAt: 2_000,
|
|
links: [{ ...packages[0].links[0], id: "replacement-link" }]
|
|
}];
|
|
|
|
const result = mergeCollectorPackages(current, incoming);
|
|
|
|
expect(result.packages[0].nameSource).toBe("explicit");
|
|
});
|
|
|
|
it("supports whole-package selection, partial transfers and partial removal", () => {
|
|
const selected = selectCollectorPackageLinks(new Set(["link-3"]), packages[0], true);
|
|
expect([...selected].sort()).toEqual(["link-1", "link-2", "link-3"]);
|
|
expect(buildCollectorTransferPackages(packages, new Set(["link-2", "link-3"])).map((pkg) => [pkg.name, pkg.links.map((link) => link.id)])).toEqual([
|
|
["SBS14HD", ["link-2"]], ["Mixed", ["link-3"]]
|
|
]);
|
|
expect(removeCollectorLinks(packages, new Set(["link-2", "link-3"])).map((pkg) => [pkg.name, pkg.links.map((link) => link.id)])).toEqual([
|
|
["SBS14HD", ["link-1"]], ["Mixed", ["link-4"]]
|
|
]);
|
|
});
|
|
|
|
it("derives aggregates, filters and search once for the view", () => {
|
|
const model = buildCollectorWorkspaceViewModel(packages, "online", "part02", false, ["link-2"], ["package-mixed"], "", true);
|
|
|
|
expect(model.packages).toHaveLength(1);
|
|
expect(model.packages[0]).toEqual(expect.objectContaining({ totalBytes: 943_718_400, unknownSizeCount: 0, onlineCount: 2, totalCount: 2, selectedCount: 1, collapsed: false }));
|
|
expect(model.packages[0].links.map((link) => link.id)).toEqual(["link-2"]);
|
|
expect(model.filters).toEqual([
|
|
{ id: "all", label: "Alle", count: 4 },
|
|
{ id: "online", label: "Online", count: 2 },
|
|
{ id: "unknown", label: "Ungeprüft", count: 1 },
|
|
{ id: "offline", label: "Offline", count: 1 }
|
|
]);
|
|
});
|
|
|
|
it("keeps initially unknown links visible while background analysis runs", () => {
|
|
const model = buildCollectorWorkspaceViewModel([packages[1]], "all", "", true, [], [], "", true);
|
|
|
|
expect(model.analyzing).toBe(true);
|
|
expect(model.empty).toBe(false);
|
|
expect(model.packages[0].links.map((link) => link.fileName)).toContain("download.bin");
|
|
});
|
|
|
|
it("toggles all package identities independent of active filters", () => {
|
|
const packageIds = packages.map((pkg) => pkg.id);
|
|
expect([...toggleAllCollectorPackageIds(packageIds, new Set())].sort()).toEqual(["package-mixed", "package-sbs"]);
|
|
expect([...toggleAllCollectorPackageIds(packageIds, new Set(["package-sbs"]))].sort()).toEqual(["package-mixed", "package-sbs"]);
|
|
expect([...toggleAllCollectorPackageIds(packageIds, new Set(packageIds))]).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("collector disclosure virtualization", () => {
|
|
it("pins only previously visible packages whose collapsed state changes", () => {
|
|
const previousCollapsed = new Map([
|
|
["package-1", false],
|
|
["package-2", false],
|
|
["package-3", false]
|
|
]);
|
|
const nextRows = [
|
|
{ id: "package-1", collapsed: true },
|
|
{ id: "package-2", collapsed: true },
|
|
{ id: "package-3", collapsed: true }
|
|
];
|
|
|
|
expect(getCollectorDisclosurePinnedIds(["package-1", "package-2"], previousCollapsed, nextRows, true)).toEqual(["package-1", "package-2"]);
|
|
expect(getCollectorDisclosurePinnedIds(["package-1", "package-2"], previousCollapsed, nextRows, false)).toEqual([]);
|
|
});
|
|
|
|
it("retains transitioning and keyboard-focused packages across virtual window changes", () => {
|
|
expect(getCollectorDisclosureViewportIds([
|
|
{ id: "visible", index: 1, pinned: false },
|
|
{ id: "transitioning", index: 99, pinned: true }
|
|
], 0, 3)).toEqual(["visible"]);
|
|
expect(mergeCollectorPinnedIds(["transitioning"], "focused")).toEqual(["transitioning", "focused"]);
|
|
expect(getCollectorFocusPackageId([
|
|
{ id: "package-1", focusIndexStart: 0, focusCount: 4 },
|
|
{ id: "package-2", focusIndexStart: 4, focusCount: 2 }
|
|
], 4)).toBe("package-2");
|
|
expect(resolveCollectorTransitionPins(["transitioning"], ["new-visible-1", "new-visible-2"], 3)).toEqual(["transitioning", "new-visible-1", "new-visible-2"]);
|
|
expect(resolveCollectorTransitionPins(["transitioning"], ["new-visible-1", "new-visible-2"], 2)).toEqual(["new-visible-1", "new-visible-2"]);
|
|
expect(resolveCollectorTransitionPins([], Array.from({ length: 12 }, (_, index) => `package-${index}`))).toEqual(Array.from({ length: 8 }, (_, index) => `package-${index + 4}`));
|
|
});
|
|
|
|
it("removes collapsed transition files from logical focus and ARIA indexing", () => {
|
|
expect(collectorFileInteractionAttributes(2, 0, 0, false)).toEqual({ rowIndex: 3, focusIndex: 2, tabIndex: undefined });
|
|
expect(collectorFileInteractionAttributes(2, 0, 0, true)).toEqual({ rowIndex: undefined, focusIndex: undefined, tabIndex: -1 });
|
|
});
|
|
});
|
|
|
|
describe("CollectorView", () => {
|
|
it("reserves the exact package height while content visibility skips offscreen rows", () => {
|
|
const expanded = buildCollectorWorkspaceViewModel([packages[0]], "all", "", false, [], [], "", true).packages[0];
|
|
const collapsed = { ...expanded, collapsed: true };
|
|
|
|
expect(collectorPackageIntrinsicBlockSize(expanded)).toBe(126);
|
|
expect(collectorPackageIntrinsicBlockSize(collapsed)).toBe(46);
|
|
expect(renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel([packages[0]], "all", "", false, [], [], "", true)} />)).toContain("contain-intrinsic-block-size:auto 126px");
|
|
});
|
|
|
|
it("keeps expanded child rows bounded to the virtual viewport for large collectors", () => {
|
|
const manyPackages: CollectorPackage[] = Array.from({ length: 50 }, (_, index) => ({
|
|
...packages[0],
|
|
id: `package-${index}`,
|
|
name: `Paket ${index}`,
|
|
links: packages[0].links.map((link, linkIndex) => ({
|
|
...link,
|
|
id: `link-${index}-${linkIndex}`,
|
|
url: `https://example.test/${index}/${linkIndex}`
|
|
}))
|
|
}));
|
|
|
|
const html = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel(manyPackages, "all", "", false, [], [], "", true)} />);
|
|
const renderedFileRows = html.match(/class="collector-file-row/g)?.length ?? 0;
|
|
|
|
expect(html).toContain("collector-virtual-spacer");
|
|
expect(renderedFileRows).toBeGreaterThan(0);
|
|
expect(renderedFileRows).toBeLessThanOrEqual(20);
|
|
});
|
|
|
|
it("does not replay the disclosure animation when an expanded virtual package mounts", () => {
|
|
const html = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel([packages[0]], "all", "", false, [], [], "", true)} />);
|
|
|
|
expect(html).toContain("collector-package-items-frame");
|
|
expect(html).not.toContain("collector-package-items-frame is-animated");
|
|
});
|
|
|
|
it("exposes logical row counts and positions for virtualized assistive navigation", () => {
|
|
const html = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel([packages[0]], "all", "", false, [], [], "", true)} />);
|
|
|
|
expect(html).toContain('aria-rowcount="4"');
|
|
expect(html).toContain('aria-rowindex="1"');
|
|
expect(html).toContain('aria-rowindex="2"');
|
|
expect(html).toContain('aria-rowindex="3"');
|
|
expect(html).toContain('aria-rowindex="4"');
|
|
expect(html).toContain('data-collector-focus-index="0"');
|
|
expect(html).toContain('data-collector-focus-index="1"');
|
|
expect(html).toContain('data-collector-focus-index="2"');
|
|
expect(html).toContain('data-collector-focus-index="3"');
|
|
});
|
|
|
|
it("renders expandable package and file rows with preview columns", () => {
|
|
const html = renderToStaticMarkup(<CollectorView actions={createActions()} model={buildCollectorWorkspaceViewModel(packages, "all", "", false, [], [], "", true)} />);
|
|
|
|
for (const heading of ["Name", "Größe", "Hoster", "Status", "Verfügbarkeit", "Hinzugefügt"]) expect(html).toContain(`>${heading}<`);
|
|
expect(html).toContain("SBS14HD");
|
|
expect(html).toContain("SBS14HD.part01.rar");
|
|
expect(html).toContain("SBS14HD.part02.rar");
|
|
expect(html).toContain("2/2 online");
|
|
expect(html).toContain(">Online<");
|
|
expect(html).toContain("aria-label=\"SBS14HD einklappen\"");
|
|
expect(html).not.toContain("URL oder Rohzeile");
|
|
expect(html).not.toContain(">Zeile<");
|
|
});
|
|
|
|
it("keeps rows and actions available during background analysis", () => {
|
|
const model = buildCollectorWorkspaceViewModel(packages, "all", "", true, ["link-1"], [], "", true);
|
|
const html = renderToStaticMarkup(<CollectorContent actions={createActions()} model={model} />);
|
|
const sidebarStatus = renderToStaticMarkup(<CollectorSidebarStatus model={model} />);
|
|
const toolbar = CollectorToolbar({ actions: createActions(), model });
|
|
|
|
expect(html).not.toContain("Analyse läuft im Hintergrund");
|
|
expect(sidebarStatus).toContain("Analyse läuft im Hintergrund");
|
|
expect(sidebarStatus).toContain('role="status"');
|
|
expect(html).toContain("SBS14HD.part01.rar");
|
|
expect(findButton(toolbar, "Auswahl übergeben (1)").props.disabled).toBe(false);
|
|
expect(findButton(toolbar, "Alle übergeben (4)").props.disabled).toBe(false);
|
|
expect(findButton(toolbar, "Auswahl entfernen").props.disabled).toBe(false);
|
|
});
|
|
|
|
it("derives status exclusively from availability instead of filename readiness", () => {
|
|
const unknownReady = [{
|
|
...packages[0],
|
|
links: [{ ...packages[0].links[0], availability: "unknown" as const, status: "ready" as const }]
|
|
}];
|
|
const partial = [{
|
|
...packages[0],
|
|
links: [
|
|
{ ...packages[0].links[0], availability: "online" as const },
|
|
{ ...packages[0].links[1], availability: "unknown" as const }
|
|
]
|
|
}];
|
|
const offlineUnknown = [{
|
|
...packages[0],
|
|
links: [
|
|
{ ...packages[0].links[0], availability: "offline" as const },
|
|
{ ...packages[0].links[1], availability: "unknown" as const }
|
|
]
|
|
}];
|
|
|
|
const unknownHtml = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel(unknownReady, "all", "", false, [], [], "", true)} />);
|
|
const partialHtml = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel(partial, "all", "", false, [], [], "", true)} />);
|
|
const offlineUnknownHtml = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel(offlineUnknown, "all", "", false, [], [], "", true)} />);
|
|
|
|
expect(unknownHtml).not.toContain(">Bereit<");
|
|
expect(unknownHtml).toContain(">Ungeprüft<");
|
|
expect(partialHtml).toContain(">Teilweise online<");
|
|
expect(partialHtml).toContain("1/2 online");
|
|
expect(offlineUnknownHtml).not.toContain(">Teilweise online<");
|
|
expect(offlineUnknownHtml).toContain(">Ungeprüft<");
|
|
});
|
|
|
|
it("renders known hosters as icons with their full name as tooltip", () => {
|
|
const html = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel(packages, "all", "", false, [], [], "", true)} />);
|
|
|
|
expect(html).toContain('class="collector-hoster-label" title="1Fichier"');
|
|
expect(html).toContain('class="collector-hoster-icon" data-hoster="1fichier" src="./provider-icons/onefichier.png"');
|
|
});
|
|
|
|
it("renders collapsed packages without child rows when animations are disabled", () => {
|
|
const html = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel(packages, "all", "", false, [], ["package-sbs"], "", false)} />);
|
|
expect(html).toContain("aria-label=\"SBS14HD ausklappen\"");
|
|
expect(html).not.toContain("SBS14HD.part01.rar");
|
|
});
|
|
|
|
it("removes collapsed child rows from the DOM even when animations are enabled", () => {
|
|
const html = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel([packages[0]], "all", "", false, [], ["package-sbs"], "", true)} />);
|
|
expect(html).not.toContain("collector-package-items-frame");
|
|
expect(html).not.toContain("SBS14HD.part01.rar");
|
|
});
|
|
|
|
it("reuses the collector content when app snapshots keep the same collector model", () => {
|
|
const model = buildCollectorWorkspaceViewModel(packages, "all", "", false, [], [], "", true);
|
|
const compare = (MemoizedCollectorContent as unknown as {
|
|
compare: (previous: { model: typeof model; actions: CollectorViewActions }, next: { model: typeof model; actions: CollectorViewActions }) => boolean;
|
|
}).compare;
|
|
|
|
expect(compare({ model, actions: createActions() }, { model, actions: createActions() })).toBe(true);
|
|
expect(compare({ model, actions: createActions() }, { model: { ...model, query: "neu" }, actions: createActions() })).toBe(false);
|
|
});
|
|
|
|
it("offers selected and all transfer actions", () => {
|
|
let selected = 0;
|
|
let all = 0;
|
|
const toolbar = CollectorToolbar({
|
|
actions: createActions({ onSubmitSelected: () => { selected += 1; }, onSubmitAll: () => { all += 1; } }),
|
|
model: buildCollectorWorkspaceViewModel(packages, "all", "", false, ["link-1"], [], "", true)
|
|
});
|
|
|
|
findButton(toolbar, "Auswahl übergeben (1)").props.onClick();
|
|
findButton(toolbar, "Alle übergeben (4)").props.onClick();
|
|
expect(selected).toBe(1);
|
|
expect(all).toBe(1);
|
|
});
|
|
|
|
it("renders accessible mixed package selection", () => {
|
|
const html = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel(packages, "all", "", false, ["link-1"], [], "", true)} />);
|
|
expect(html).toContain("aria-label=\"Paket SBS14HD auswählen\"");
|
|
expect(html).toContain("aria-checked=\"mixed\"");
|
|
});
|
|
|
|
it("routes status filters and search independently", () => {
|
|
let filter = "";
|
|
let query = "";
|
|
const model = buildCollectorWorkspaceViewModel(packages, "online", "", false, [], [], "", true);
|
|
const actions = createActions({ onFilterChange: (value) => { filter = value; }, onQueryChange: (value) => { query = value; } });
|
|
const sidebar = CollectorSidebar({ actions, model });
|
|
findElement(sidebar, (element) => element.type === "button" && element.props["aria-current"] === "page").props.onClick();
|
|
const toolbar = CollectorToolbar({ actions, model });
|
|
findElement(toolbar, (element) => element.props.label === "Links durchsuchen").props.onChange({ target: { value: "part02" } });
|
|
expect(filter).toBe("online");
|
|
expect(query).toBe("part02");
|
|
});
|
|
|
|
it("keeps errors visible without replacing existing packages", () => {
|
|
const html = renderToStaticMarkup(<CollectorContent actions={createActions()} model={buildCollectorWorkspaceViewModel(packages, "all", "", false, [], [], "Ein Link konnte nicht geprüft werden", true)} />);
|
|
expect(html).toContain("Ein Link konnte nicht geprüft werden");
|
|
expect(html).toContain("SBS14HD.part01.rar");
|
|
});
|
|
|
|
it("uses an analysis dialog instead of a raw tab editor", () => {
|
|
let value = "";
|
|
let commits = 0;
|
|
const dialog = CollectorInputDialog({ open: true, value, onChange: (next) => { value = next; }, onClose: () => {}, onCommit: () => { commits += 1; } });
|
|
const html = renderToStaticMarkup(dialog);
|
|
expect(html).toContain("Links erscheinen sofort und werden anschließend im Hintergrund geprüft.");
|
|
expect(html).toContain("Hinzufügen");
|
|
findElement(dialog, (element) => element.type === "textarea").props.onChange({ target: { value: "https://1fichier.com/?abc" } });
|
|
findButton(dialog, "Hinzufügen").props.onClick();
|
|
expect(value).toBe("https://1fichier.com/?abc");
|
|
expect(commits).toBe(1);
|
|
});
|
|
|
|
it("uses aligned responsive package grids and positioned virtual rows", () => {
|
|
const css = readFileSync(new URL("../src/renderer/views/collector/collector.css", import.meta.url), "utf8");
|
|
expect(css).toMatch(/\.collector-table-header-row,\s*\.collector-package-row,\s*\.collector-file-row\s*\{[^}]*grid-template-columns:/s);
|
|
expect(css).toMatch(/\.collector-virtual-package\s*\{[^}]*position:\s*absolute;[^}]*transform:\s*translateY/s);
|
|
expect(css).toMatch(/@media \(max-width: 1120px\)[\s\S]*\.collector-toolbar\s*\{[^}]*flex-wrap:\s*wrap;/s);
|
|
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.collector-virtual-spacer\.is-motion-enabled \.collector-virtual-package\s*\{[^}]*transition-duration:\s*300ms, 300ms !important;/s);
|
|
expect(css).toMatch(/\.collector-package-items-frame\.is-expanding\s*\{[^}]*animation:\s*collector-items-expand/s);
|
|
expect(css).not.toMatch(/\.collector-package-items-frame\.is-animated\s*\{[^}]*animation:\s*collector-items-expand/s);
|
|
expect(css).toMatch(/\.collector-table-header-row > span:nth-child\(2\)\s*\{[^}]*padding-left:\s*48px;/s);
|
|
expect(css).toMatch(/\.collector-name-cell\.is-file\s*\{[^}]*padding-left:\s*32px;/s);
|
|
expect(css).toMatch(/\.collector-table-header,\s*\.collector-table-body\s*\{[^}]*scrollbar-gutter:\s*stable;/s);
|
|
expect(css).toMatch(/\.collector-size-cell,\s*\.collector-hoster-cell,\s*\.collector-status-cell,\s*\.collector-availability-cell,\s*\.collector-added-cell\s*\{[^}]*text-align:\s*center;/s);
|
|
expect(css).toMatch(/\.collector-table-header-row > span:nth-child\(n\+3\)\s*\{[^}]*justify-content:\s*center;[^}]*text-align:\s*center;/s);
|
|
expect(collectorHeaderScrollStyle(37)).toEqual({ transform: "translateX(-37px)" });
|
|
});
|
|
|
|
it("uses one consistent gap across collector toolbar groups", () => {
|
|
const css = readFileSync(new URL("../src/renderer/views/collector/collector.css", import.meta.url), "utf8");
|
|
const toolbarGap = css.match(/\.collector-toolbar\s*\{[^}]*gap:\s*([^;]+);/s)?.[1]?.trim();
|
|
const groupGap = css.match(/\.collector-toolbar \.ui-toolbar-group\s*\{[^}]*gap:\s*([^;]+);/s)?.[1]?.trim();
|
|
expect(toolbarGap).toBeTruthy();
|
|
expect(groupGap).toBe(toolbarGap);
|
|
});
|
|
|
|
it("uses the green confirmation style for the add-links action", () => {
|
|
const css = readFileSync(new URL("../src/renderer/views/collector/collector.css", import.meta.url), "utf8");
|
|
|
|
expect(css).toMatch(/\.collector-action-primary\s*\{[^}]*border-color:\s*var\(--ui-success\);[^}]*background:\s*var\(--ui-success\);[^}]*color:\s*#000000;/s);
|
|
expect(css).toMatch(/\.collector-action-primary:hover:not\(:disabled\)\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--ui-success\) 86%, #ffffff\);[^}]*color:\s*#000000;/s);
|
|
});
|
|
});
|