feat(collector): rebuild link preview as package workspace
Inspect pasted links, text files and DLC containers before queue insertion and resolve supported hoster metadata without starting downloads. Group multipart files into expandable packages with size, status, availability, timestamps, filtering and stable selection. Add selected or complete transfer to Downloads while retaining entries on failed or partial handoff. Keep collector toolbar spacing consistent and show the full 1Fichier identity with its provider icon in Downloads.
This commit is contained in:
@@ -109,4 +109,17 @@ describe("account preload contract", () => {
|
||||
expect(electron.invoke).toHaveBeenCalledWith(IPC_CHANNELS.GET_ARCHIVE_PASSWORD_LIST);
|
||||
expect(result).toEqual({ passwords });
|
||||
});
|
||||
|
||||
it("forwards collector text and container inspection without adding downloads", async () => {
|
||||
const result = { packages: [], invalidCount: 0, duplicateCount: 0 };
|
||||
electron.invoke.mockResolvedValue(result);
|
||||
|
||||
await electron.api?.inspectCollectorText({ rawText: "https://1fichier.com/?abc12345", addedAt: 1234 });
|
||||
await electron.api?.inspectCollectorContainers(["C:\\Imports\\sample.dlc"], 5678);
|
||||
|
||||
expect(electron.invoke.mock.calls).toEqual([
|
||||
[IPC_CHANNELS.INSPECT_COLLECTOR_TEXT, { rawText: "https://1fichier.com/?abc12345", addedAt: 1234 }],
|
||||
[IPC_CHANNELS.INSPECT_COLLECTOR_CONTAINERS, ["C:\\Imports\\sample.dlc"], 5678]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,12 +18,14 @@ describe("desktop shell", () => {
|
||||
expect(source).toContain("Maskierte Kennung kopiert");
|
||||
});
|
||||
|
||||
it("confirms before removing a collector tab", () => {
|
||||
it("removes collector links only after the exact download transfer succeeds", () => {
|
||||
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8");
|
||||
const removal = source.slice(source.indexOf("const removeCollectorTab"), source.indexOf("const openCollectorInput"));
|
||||
const start = source.indexOf("const transferCollectorPackages");
|
||||
const transfer = source.slice(start, source.indexOf("const onImportDlc =", start));
|
||||
|
||||
expect(removal).toContain("askConfirmPrompt");
|
||||
expect(removal.indexOf("askConfirmPrompt")).toBeLessThan(removal.indexOf("planCollectorTabRemoval"));
|
||||
expect(transfer).toContain("await window.rd.addLinks");
|
||||
expect(transfer).toContain("result.addedLinks !== transferredIds.size");
|
||||
expect(transfer.indexOf("await window.rd.addLinks")).toBeLessThan(transfer.indexOf("setCollectorPackages"));
|
||||
});
|
||||
|
||||
it("confirms before removing selected collector links", () => {
|
||||
@@ -31,7 +33,7 @@ describe("desktop shell", () => {
|
||||
const removal = source.slice(source.indexOf("const removeSelectedCollectorRows"), source.indexOf("const onPackageStartEdit"));
|
||||
|
||||
expect(removal).toContain("askConfirmPrompt");
|
||||
expect(removal.indexOf("askConfirmPrompt")).toBeLessThan(removal.indexOf("setCollectorTabs"));
|
||||
expect(removal.indexOf("askConfirmPrompt")).toBeLessThan(removal.indexOf("setCollectorPackages"));
|
||||
expect(removal).toContain('title: "Ausgewählte Links löschen"');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import {
|
||||
inferCollectorPackageName,
|
||||
inspectCollectorText,
|
||||
serializeCollectorPackages
|
||||
} from "../src/main/collector-inspection";
|
||||
|
||||
const sbsLinks = [
|
||||
"https://1fichier.com/?82xiit09yax8npoh1qt8",
|
||||
"https://1fichier.com/?gr4ry0k18p3sg74vupfp",
|
||||
"https://1fichier.com/?bui5smo6gsxxelz49ukl",
|
||||
"https://1fichier.com/?9d3udo3rr96f8xi2xj74",
|
||||
"https://1fichier.com/?lqetjbffrw58zrxsrz00",
|
||||
"https://1fichier.com/?0cgbn7se8l4as8sopr9u",
|
||||
"https://1fichier.com/?dufuuwir5skm055penoo",
|
||||
"https://1fichier.com/?p2njgtuhtuzm20vwp4n5",
|
||||
"https://1fichier.com/?ogqhzkqpj2ugmm9evc11",
|
||||
"https://1fichier.com/?ktymbac2nt78o5zsi3z5",
|
||||
"https://1fichier.com/?q51ktq0jb7fmez42n3zs",
|
||||
"https://1fichier.com/?oo7p0wdnapdix2dvt6d0",
|
||||
"https://1fichier.com/?lv2s37fkloo7wgjwq05r",
|
||||
"https://1fichier.com/?fnczcnoljqxgny9q7fxt",
|
||||
"https://1fichier.com/?pbg9n0rrqk3rpnflqbe8",
|
||||
"https://1fichier.com/?cgmdxz11f2gd9u7by9tg"
|
||||
];
|
||||
|
||||
describe("collector inspection", () => {
|
||||
it("groups the real SBS14HD multipart shape into one package with exact metadata", async () => {
|
||||
const metadata = new Map(sbsLinks.map((link, index) => [link, {
|
||||
online: true,
|
||||
fileName: `SBS14HD.part${String(index + 1).padStart(2, "0")}.rar`,
|
||||
fileSizeBytes: index === 15 ? 373_517_856 : 471_859_200,
|
||||
accessRestricted: false
|
||||
}]));
|
||||
|
||||
const result = await inspectCollectorText({ rawText: sbsLinks.join("\n"), addedAt: 1_777_777_777_000 }, defaultSettings(), {
|
||||
checkOneFichier: async () => metadata,
|
||||
createId: (() => {
|
||||
let value = 0;
|
||||
return (prefix) => `${prefix}-${++value}`;
|
||||
})()
|
||||
});
|
||||
|
||||
expect(result.invalidCount).toBe(0);
|
||||
expect(result.duplicateCount).toBe(0);
|
||||
expect(result.packages).toHaveLength(1);
|
||||
expect(result.packages[0].name).toBe("SBS14HD");
|
||||
expect(result.packages[0].links).toHaveLength(16);
|
||||
expect(result.packages[0].links.map((link) => link.fileName)).toEqual(
|
||||
Array.from({ length: 16 }, (_unused, index) => `SBS14HD.part${String(index + 1).padStart(2, "0")}.rar`)
|
||||
);
|
||||
expect(result.packages[0].links.reduce((sum, link) => sum + (link.fileSizeBytes || 0), 0)).toBe(7_451_405_856);
|
||||
expect(result.packages[0].links.every((link) => link.hoster === "1fichier" && link.availability === "online" && link.status === "ready")).toBe(true);
|
||||
expect(result.packages[0].addedAt).toBe(1_777_777_777_000);
|
||||
});
|
||||
|
||||
it("keeps explicit packages authoritative while removing duplicate URLs", async () => {
|
||||
const rawText = [
|
||||
"# Package: Staffel A",
|
||||
"# File: episode.part01.rar",
|
||||
"https://example.com/a",
|
||||
"https://example.com/a",
|
||||
"# Package: Staffel B",
|
||||
"https://example.com/b"
|
||||
].join("\n");
|
||||
|
||||
const result = await inspectCollectorText({ rawText, addedAt: 2000 }, defaultSettings(), {
|
||||
resolveFilenames: async () => new Map([["https://example.com/b", "episode.part02.rar"]])
|
||||
});
|
||||
|
||||
expect(result.packages.map((pkg) => pkg.name)).toEqual(["Staffel A", "Staffel B"]);
|
||||
expect(result.packages.map((pkg) => pkg.links.length)).toEqual([1, 1]);
|
||||
expect(result.packages[0].links[0].fileName).toBe("episode.part01.rar");
|
||||
expect(result.packages[1].links[0].fileName).toBe("episode.part02.rar");
|
||||
expect(result.duplicateCount).toBe(1);
|
||||
});
|
||||
|
||||
it("retains offline and unknown links as visible collector entries", async () => {
|
||||
const offline = "https://1fichier.com/?offline123";
|
||||
const unknown = "https://unknown.example/resource";
|
||||
const result = await inspectCollectorText({ rawText: `${offline}\n${unknown}`, addedAt: 3000 }, defaultSettings(), {
|
||||
checkOneFichier: async () => new Map([[offline, {
|
||||
online: false,
|
||||
fileName: "",
|
||||
fileSizeBytes: null,
|
||||
accessRestricted: false
|
||||
}]]),
|
||||
resolveFilenames: async () => new Map()
|
||||
});
|
||||
const links = result.packages.flatMap((pkg) => pkg.links);
|
||||
|
||||
expect(links.find((link) => link.url === offline)).toEqual(expect.objectContaining({
|
||||
availability: "offline",
|
||||
status: "offline"
|
||||
}));
|
||||
expect(links.find((link) => link.url === unknown)).toEqual(expect.objectContaining({
|
||||
availability: "unknown",
|
||||
status: "unknown"
|
||||
}));
|
||||
});
|
||||
|
||||
it("infers archive package names and serializes inspected metadata for the existing queue parser", () => {
|
||||
expect(inferCollectorPackageName("SBS14HD.part01.rar", "1fichier")).toBe("SBS14HD");
|
||||
expect(inferCollectorPackageName("Archive.7z.001", "1fichier")).toBe("Archive");
|
||||
expect(inferCollectorPackageName("Show.r00", "1fichier")).toBe("Show");
|
||||
|
||||
const serialized = serializeCollectorPackages([{
|
||||
id: "package-1",
|
||||
name: "SBS14HD",
|
||||
addedAt: 4000,
|
||||
links: [{
|
||||
id: "link-1",
|
||||
url: sbsLinks[0],
|
||||
fileName: "SBS14HD.part01.rar",
|
||||
fileSizeBytes: 471_859_200,
|
||||
hoster: "1fichier",
|
||||
availability: "online",
|
||||
status: "ready",
|
||||
addedAt: 4000
|
||||
}]
|
||||
}]);
|
||||
|
||||
expect(serialized).toBe(`# Package: SBS14HD\n# File: SBS14HD.part01.rar\n${sbsLinks[0]}`);
|
||||
});
|
||||
});
|
||||
+181
-304
@@ -1,326 +1,203 @@
|
||||
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 {
|
||||
mergeCollectorDraftText,
|
||||
planCollectorTabRemoval,
|
||||
planCollectorTextReplacement
|
||||
} from "../src/renderer/App";
|
||||
import {
|
||||
buildCollectorRows,
|
||||
buildCollectorViewModel,
|
||||
type CollectorSourceTab
|
||||
} from "../src/renderer/views/collector/collector-model";
|
||||
import {
|
||||
CollectorInputDialog,
|
||||
CollectorContent,
|
||||
CollectorSidebar,
|
||||
CollectorToolbar,
|
||||
CollectorView,
|
||||
type CollectorViewActions
|
||||
} from "../src/renderer/views/collector/CollectorView";
|
||||
|
||||
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 {
|
||||
onTabSelect: () => {},
|
||||
onTabAdd: () => {},
|
||||
onTabRemove: () => {},
|
||||
onOpenInput: () => {},
|
||||
onImportDlc: () => {},
|
||||
onImportFile: () => {},
|
||||
onExportQueue: () => {},
|
||||
onSubmit: () => {},
|
||||
onQueryChange: () => {},
|
||||
onSelectionChange: () => {},
|
||||
onRemoveSelected: () => {},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
const populatedTabs: CollectorSourceTab[] = [
|
||||
{
|
||||
id: "tab-a",
|
||||
name: "Sammlung A",
|
||||
text: "https://example.test/a\n\n https://example.test/b "
|
||||
}
|
||||
];
|
||||
|
||||
describe("collector model", () => {
|
||||
it("derives stable rows from non-empty raw lines without validating or regrouping them", () => {
|
||||
const rows = buildCollectorRows(populatedTabs, "tab-a", "");
|
||||
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows.map((row) => row.id)).toEqual(["tab-a:0", "tab-a:2"]);
|
||||
expect(rows.map((row) => row.originalLineIndex)).toEqual([0, 2]);
|
||||
expect(rows.map((row) => row.value)).toEqual([
|
||||
"https://example.test/a",
|
||||
"https://example.test/b"
|
||||
]);
|
||||
expect(rows[0].linkCount).toBe(2);
|
||||
expect(rows[1].linkCount).toBe(2);
|
||||
});
|
||||
|
||||
it("filters presentation rows while keeping source counts and original line identities", () => {
|
||||
const model = buildCollectorViewModel(populatedTabs, "tab-a", "EXAMPLE.TEST/B", false, ["tab-a:0"]);
|
||||
|
||||
expect(model.rows.map((row) => row.id)).toEqual(["tab-a:2"]);
|
||||
expect(model.tabs).toEqual([{ id: "tab-a", name: "Sammlung A", linkCount: 2 }]);
|
||||
expect(model.selectedIds).toEqual(["tab-a:0"]);
|
||||
expect(model.empty).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves clipboard and drop appends that arrive while an input draft is open", () => {
|
||||
expect(mergeCollectorDraftText(
|
||||
"https://example.test/old",
|
||||
"https://example.test/old\nhttps://example.test/clipboard",
|
||||
"https://example.test/edited"
|
||||
)).toBe("https://example.test/edited\nhttps://example.test/clipboard");
|
||||
expect(mergeCollectorDraftText("old", "old", "edited")).toBe("edited");
|
||||
});
|
||||
|
||||
it("moves the active identity to an existing neighbor before later appends arrive", () => {
|
||||
const tabs: CollectorSourceTab[] = [
|
||||
{ id: "tab-a", name: "Sammlung A", text: "a" },
|
||||
{ id: "tab-b", name: "Sammlung B", text: "b" },
|
||||
{ id: "tab-c", name: "Sammlung C", text: "c" }
|
||||
];
|
||||
|
||||
expect(planCollectorTabRemoval(tabs, "tab-b", "tab-b")).toEqual({
|
||||
tabs: [tabs[0], tabs[2]],
|
||||
activeTabId: "tab-a"
|
||||
});
|
||||
expect(planCollectorTabRemoval(tabs, "tab-c", "tab-a")).toEqual({
|
||||
tabs: [tabs[1], tabs[2]],
|
||||
activeTabId: "tab-c"
|
||||
});
|
||||
});
|
||||
|
||||
it("invalidates positional row selection whenever raw text is replaced", () => {
|
||||
const tabs: CollectorSourceTab[] = [
|
||||
{ id: "tab-a", name: "Sammlung A", text: "old-a\nold-b" },
|
||||
{ id: "tab-b", name: "Sammlung B", text: "untouched" }
|
||||
];
|
||||
|
||||
expect(planCollectorTextReplacement(tabs, "tab-a", "new-a")).toEqual({
|
||||
tabs: [
|
||||
{ id: "tab-a", name: "Sammlung A", text: "new-a" },
|
||||
tabs[1]
|
||||
],
|
||||
selectedIds: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CollectorView", () => {
|
||||
it("marks collections for one measured vertical selection indicator", () => {
|
||||
const model = buildCollectorViewModel([
|
||||
{ id: "tab-a", name: "Sammlung A", text: "https://example.test/a" },
|
||||
{ id: "tab-b", name: "Sammlung B", text: "https://example.test/b" }
|
||||
], "tab-b", "", false, []);
|
||||
const html = renderToStaticMarkup(<CollectorSidebar actions={createActions()} model={model} />);
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CollectorPackage } from "../src/shared/collector";
|
||||
import {
|
||||
buildCollectorTransferPackages,
|
||||
buildCollectorWorkspaceViewModel,
|
||||
mergeCollectorPackages,
|
||||
removeCollectorLinks,
|
||||
selectCollectorPackageLinks
|
||||
} from "../src/renderer/views/collector/collector-model";
|
||||
import { CollectorContent, CollectorInputDialog, CollectorSidebar, CollectorToolbar, CollectorView, type CollectorViewActions } from "../src/renderer/views/collector/CollectorView";
|
||||
|
||||
expect(html).toContain("ui-sliding-selection ui-sliding-selection-vertical");
|
||||
expect(html.match(/data-sliding-selection-item="true"/g)).toHaveLength(2);
|
||||
expect(html.match(/data-sliding-selection-active="true"/g)).toHaveLength(1);
|
||||
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: () => {},
|
||||
onRemoveSelected: () => {},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
const packages: CollectorPackage[] = [{
|
||||
id: "package-sbs",
|
||||
name: "SBS14HD",
|
||||
addedAt: 1000,
|
||||
links: [
|
||||
{ id: "link-1", url: "https://1fichier.com/?one11111", fileName: "SBS14HD.part01.rar", fileSizeBytes: 471_859_200, hoster: "1fichier", availability: "online", status: "ready", addedAt: 1000 },
|
||||
{ id: "link-2", url: "https://1fichier.com/?two22222", fileName: "SBS14HD.part02.rar", fileSizeBytes: 471_859_200, hoster: "1fichier", availability: "online", status: "ready", addedAt: 1000 }
|
||||
]
|
||||
}, {
|
||||
id: "package-mixed",
|
||||
name: "Mixed",
|
||||
addedAt: 2000,
|
||||
links: [
|
||||
{ id: "link-3", url: "https://example.test/unknown", fileName: "unknown.bin", fileSizeBytes: null, hoster: "example", availability: "unknown", status: "unknown", addedAt: 2000 },
|
||||
{ id: "link-4", url: "https://example.test/offline", fileName: "offline.bin", fileSizeBytes: null, hoster: "example", availability: "offline", status: "offline", addedAt: 2000 }
|
||||
]
|
||||
}];
|
||||
|
||||
describe("collector workspace model", () => {
|
||||
it("merges packages by name while deduplicating URLs", () => {
|
||||
const incoming: CollectorPackage[] = [{
|
||||
id: "incoming",
|
||||
name: "SBS14HD",
|
||||
addedAt: 3000,
|
||||
links: [
|
||||
{ ...packages[0].links[0], id: "duplicate" },
|
||||
{ id: "link-5", url: "https://1fichier.com/?three333", fileName: "SBS14HD.part03.rar", fileSizeBytes: 10, hoster: "1fichier", availability: "online", status: "ready", addedAt: 3000 }
|
||||
]
|
||||
}];
|
||||
const result = mergeCollectorPackages(packages, incoming);
|
||||
|
||||
expect(result.addedLinks).toBe(1);
|
||||
expect(result.duplicateLinks).toBe(1);
|
||||
expect(result.packages[0].id).toBe("package-sbs");
|
||||
expect(result.packages[0].links.map((link) => link.id)).toEqual(["link-1", "link-2", "link-5"]);
|
||||
});
|
||||
|
||||
it("keeps empty, busy and error states inside the same table body", () => {
|
||||
const empty = renderToStaticMarkup(
|
||||
<CollectorView
|
||||
actions={createActions()}
|
||||
model={buildCollectorViewModel([{ id: "tab-a", name: "Sammlung A", text: "" }], "tab-a", "", false, [])}
|
||||
/>
|
||||
);
|
||||
const busy = renderToStaticMarkup(
|
||||
<CollectorView
|
||||
actions={createActions()}
|
||||
model={{ ...buildCollectorViewModel([], "", "", true, []), error: "" }}
|
||||
/>
|
||||
);
|
||||
const failed = renderToStaticMarkup(
|
||||
<CollectorView
|
||||
actions={createActions()}
|
||||
model={{ ...buildCollectorViewModel([], "", "", false, []), error: "Import fehlgeschlagen" }}
|
||||
/>
|
||||
);
|
||||
|
||||
for (const [html, state] of [
|
||||
[empty, "Noch keine Links"],
|
||||
[busy, "Links werden verarbeitet"],
|
||||
[failed, "Import fehlgeschlagen"]
|
||||
]) {
|
||||
expect(html.indexOf(state)).toBeGreaterThan(html.indexOf("data-visual-region=\"collector-table-body\""));
|
||||
}
|
||||
expect(empty).toContain("data-visual-region=\"collector-empty-state\"");
|
||||
expect(empty).not.toContain("aria-label=\"Seitennavigation\"");
|
||||
});
|
||||
|
||||
it("renders compact occupied rows and removes the empty marker", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CollectorView
|
||||
actions={createActions()}
|
||||
model={buildCollectorViewModel(populatedTabs, "tab-a", "", false, [])}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html.match(/class=\"collector-row(?: is-selected)?\"/g)).toHaveLength(2);
|
||||
expect(html).not.toContain("data-visual-region=\"collector-empty-state\"");
|
||||
expect(html).toContain("data-visual-region=\"collector-sidebar\"");
|
||||
expect(html).toContain("data-visual-region=\"collector-toolbar\"");
|
||||
expect(html).toContain("data-visual-region=\"collector-table-body\"");
|
||||
expect(html).not.toContain("data-visual-region=\"downloads-toolbar\"");
|
||||
expect(html).not.toContain("aria-label=\"Seitennavigation\"");
|
||||
});
|
||||
|
||||
it("gives every row checkbox a unique accessible name with its link and collection", () => {
|
||||
const content = CollectorContent({
|
||||
actions: createActions(),
|
||||
model: buildCollectorViewModel(populatedTabs, "tab-a", "", false, [])
|
||||
});
|
||||
const labels: string[] = [];
|
||||
visitElements(content, (element) => {
|
||||
if (element.type === "input" && element.props.type === "checkbox") {
|
||||
labels.push(element.props["aria-label"]);
|
||||
}
|
||||
});
|
||||
|
||||
expect(labels).toEqual([
|
||||
"https://example.test/a aus Sammlung A, Zeile 1 auswählen",
|
||||
"https://example.test/b aus Sammlung A, Zeile 3 auswählen"
|
||||
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"]]
|
||||
]);
|
||||
expect(new Set(labels).size).toBe(labels.length);
|
||||
});
|
||||
|
||||
it("uses a high-contrast table heading token in both themes", () => {
|
||||
const css = readFileSync(new URL("../src/renderer/views/collector/collector.css", import.meta.url), "utf8");
|
||||
it("derives aggregates, filters and search once for the view", () => {
|
||||
const model = buildCollectorWorkspaceViewModel(packages, "online", "part02", false, ["link-2"], ["package-mixed"], "", true);
|
||||
|
||||
expect(css).toMatch(/\.collector-table-header-row\s*{[^}]*color:\s*var\(--ui-text-secondary\);/s);
|
||||
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 Links", count: 4 }, { id: "online", label: "Online", count: 2 }, { id: "unknown", label: "Ungeprüft", count: 1 }, { id: "offline", label: "Offline", count: 1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CollectorView", () => {
|
||||
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("aria-label=\"SBS14HD einklappen\"");
|
||||
expect(html).not.toContain("URL oder Rohzeile");
|
||||
expect(html).not.toContain(">Zeile<");
|
||||
expect(html).not.toContain(">Lokal<");
|
||||
});
|
||||
|
||||
it("uses the semantic danger text token for the removal action", () => {
|
||||
const css = readFileSync(new URL("../src/renderer/views/collector/collector.css", import.meta.url), "utf8");
|
||||
|
||||
expect(css).toMatch(/\.collector-action-danger:not\(:disabled\)\s*{[^}]*color:\s*var\(--ui-danger-text\);/s);
|
||||
it("renders collapsed packages without child rows", () => {
|
||||
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("disables queue submission only when the active collection has no links", () => {
|
||||
const emptyActive = CollectorToolbar({
|
||||
actions: createActions(),
|
||||
model: buildCollectorViewModel([
|
||||
{ id: "tab-a", name: "Sammlung A", text: "" },
|
||||
{ id: "tab-b", name: "Sammlung B", text: "https://example.test/b" }
|
||||
], "tab-a", "", false, [])
|
||||
});
|
||||
const filteredActive = CollectorToolbar({
|
||||
actions: createActions(),
|
||||
model: buildCollectorViewModel(populatedTabs, "tab-a", "kein-treffer", false, [])
|
||||
});
|
||||
|
||||
expect(findButton(emptyActive, "An Downloads übergeben").props.disabled).toBe(true);
|
||||
expect(findButton(filteredActive, "An Downloads übergeben").props.disabled).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("separates local input, queue submission, search, selection and local removal callbacks", () => {
|
||||
let inputOpens = 0;
|
||||
let queueSubmits = 0;
|
||||
let query = "";
|
||||
let selected = "";
|
||||
let removals = 0;
|
||||
const actions = createActions({
|
||||
onOpenInput: () => { inputOpens += 1; },
|
||||
onSubmit: () => { queueSubmits += 1; },
|
||||
onQueryChange: (value) => { query = value; },
|
||||
onSelectionChange: (rowId) => { selected = rowId; },
|
||||
onRemoveSelected: () => { removals += 1; }
|
||||
});
|
||||
const toolbar = CollectorToolbar({
|
||||
actions,
|
||||
model: buildCollectorViewModel(populatedTabs, "tab-a", "", false, ["tab-a:0"])
|
||||
});
|
||||
const content = CollectorContent({
|
||||
actions,
|
||||
model: buildCollectorViewModel(populatedTabs, "tab-a", "", false, ["tab-a:0"])
|
||||
});
|
||||
|
||||
findButton(toolbar, "Links hinzufügen").props.onClick();
|
||||
expect(inputOpens).toBe(1);
|
||||
expect(queueSubmits).toBe(0);
|
||||
|
||||
findButton(toolbar, "An Downloads übergeben").props.onClick();
|
||||
expect(queueSubmits).toBe(1);
|
||||
|
||||
const search = findElement(toolbar, (element) => element.props.label === "Links durchsuchen");
|
||||
search.props.onChange({ target: { value: "release" } });
|
||||
expect(query).toBe("release");
|
||||
|
||||
const checkbox = findElement(content, (element) => element.type === "input" && element.props.type === "checkbox");
|
||||
checkbox.props.onChange();
|
||||
findButton(toolbar, "Auswahl entfernen").props.onClick();
|
||||
expect(selected).toBe("tab-a:0");
|
||||
expect(removals).toBe(1);
|
||||
expect(queueSubmits).toBe(1);
|
||||
});
|
||||
|
||||
it("names the input dialog and commits only through the local draft callback", () => {
|
||||
let value = "";
|
||||
let commits = 0;
|
||||
const dialog = CollectorInputDialog({
|
||||
open: true,
|
||||
tabName: "Sammlung A",
|
||||
value,
|
||||
onChange: (next) => { value = next; },
|
||||
onClose: () => {},
|
||||
onCommit: () => { commits += 1; }
|
||||
});
|
||||
const html = renderToStaticMarkup(dialog);
|
||||
|
||||
expect(html).toContain("role=\"dialog\"");
|
||||
expect(html).toContain("aria-label=\"Links\"");
|
||||
expect(html).toContain("Links hinzufügen");
|
||||
expect(html).toContain("Übernehmen");
|
||||
|
||||
const textbox = findElement(dialog, (element) => element.type === "textarea" && element.props["aria-label"] === "Links");
|
||||
textbox.props.onChange({ target: { value: "https://example.test/new" } });
|
||||
findButton(dialog, "Übernehmen").props.onClick();
|
||||
expect(value).toBe("https://example.test/new");
|
||||
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 busy, error and empty states inside the table body", () => {
|
||||
const actions = createActions();
|
||||
const empty = renderToStaticMarkup(<CollectorContent actions={actions} model={buildCollectorWorkspaceViewModel([], "all", "", false, [], [], "", true)} />);
|
||||
const busy = renderToStaticMarkup(<CollectorContent actions={actions} model={buildCollectorWorkspaceViewModel([], "all", "", true, [], [], "", true)} />);
|
||||
const failed = renderToStaticMarkup(<CollectorContent actions={actions} model={buildCollectorWorkspaceViewModel([], "all", "", false, [], [], "Import fehlgeschlagen", true)} />);
|
||||
expect(empty).toContain("Noch keine Links");
|
||||
expect(busy).toContain("Links werden analysiert");
|
||||
expect(failed).toContain("Import fehlgeschlagen");
|
||||
});
|
||||
|
||||
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 werden geprüft und automatisch zu Downloadpaketen gruppiert.");
|
||||
expect(html).toContain("Analysieren");
|
||||
findElement(dialog, (element) => element.type === "textarea").props.onChange({ target: { value: "https://1fichier.com/?abc" } });
|
||||
findButton(dialog, "Analysieren").props.onClick();
|
||||
expect(value).toBe("https://1fichier.com/?abc");
|
||||
expect(commits).toBe(1);
|
||||
});
|
||||
|
||||
it("moves the search field onto a separate compact row instead of overlapping actions", () => {
|
||||
it("uses aligned responsive package grids and content visibility", () => {
|
||||
const css = readFileSync(new URL("../src/renderer/views/collector/collector.css", import.meta.url), "utf8");
|
||||
|
||||
expect(css).toMatch(/\.collector-table-header-row, \.collector-package-row, \.collector-file-row\s*\{[^}]*grid-template-columns:/s);
|
||||
expect(css).toMatch(/\.collector-package-group\s*\{[^}]*content-visibility:\s*auto;/s);
|
||||
expect(css).toMatch(/@media \(max-width: 1120px\)[\s\S]*\.collector-toolbar\s*\{[^}]*flex-wrap:\s*wrap;/s);
|
||||
expect(css).toMatch(/@media \(max-width: 1120px\)[\s\S]*\.collector-toolbar \.ui-toolbar-search\s*\{[^}]*flex:\s*1 0 100%;[^}]*width:\s*100%;/s);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -630,7 +630,7 @@ describe("responsive Downloadstatus und Servicebezeichnungen", () => {
|
||||
|
||||
expect(hosters).toEqual(["1fichier", "1fichier", "1fichier", "1fichier"]);
|
||||
expect(new Set(hosters).size).toBe(1);
|
||||
expect(formatHosterLabel(hosters[1])).toEqual({ compact: "1F", title: "1Fichier" });
|
||||
expect(formatHosterLabel(hosters[1])).toEqual({ compact: "1Fichier", title: "1Fichier", iconSrc: "./provider-icons/onefichier.png" });
|
||||
});
|
||||
|
||||
it("removes duplicated access-mode wording from service labels", () => {
|
||||
|
||||
@@ -63,6 +63,11 @@ describe("renderer localization", () => {
|
||||
expect(translateUiText("Einträge: 2", "en")).toBe("Entries: 2");
|
||||
expect(translateUiText("Sichtbar: 2", "en")).toBe("Visible: 2");
|
||||
expect(translateUiText("Ausgewählt: 0", "en")).toBe("Selected: 0");
|
||||
expect(translateUiText("Auswahl übergeben (3)", "en")).toBe("Send selection (3)");
|
||||
expect(translateUiText("Alle übergeben (16)", "en")).toBe("Send all (16)");
|
||||
expect(translateUiText("Paket SBS14HD auswählen", "en")).toBe("Select package SBS14HD");
|
||||
expect(translateUiText("16 Dateien", "en")).toBe("16 files");
|
||||
expect(translateUiText("15/16 geprüft", "en")).toBe("15/16 checked");
|
||||
expect(translateUiText("2 pro Seite", "en")).toBe("2 per page");
|
||||
expect(translateUiText("Sichtbar: ", "en")).toBe("Visible: ");
|
||||
expect(translateUiText(" pro Seite", "en")).toBe(" per page");
|
||||
|
||||
@@ -3,6 +3,7 @@ import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { assertTrustedIpcSender } from "../src/main/ipc-security";
|
||||
import { validateRealDebridLoginRequest } from "../src/shared/preload-api";
|
||||
import { validateCollectorContainerInspectionRequest, validateCollectorInspectionRequest } from "../src/shared/collector";
|
||||
|
||||
function eventFor(url: string) {
|
||||
return {
|
||||
@@ -25,6 +26,27 @@ describe("ipc-security", () => {
|
||||
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_existing", dailyLimitBytes: 1 })).toThrow(/Account-Payload/i);
|
||||
});
|
||||
|
||||
it("accepts only bounded collector inspection payloads", () => {
|
||||
expect(validateCollectorInspectionRequest({ rawText: "https://1fichier.com/?abc12345", addedAt: 1234 })).toEqual({
|
||||
rawText: "https://1fichier.com/?abc12345",
|
||||
addedAt: 1234
|
||||
});
|
||||
expect(() => validateCollectorInspectionRequest({ rawText: "x", addedAt: 1, token: "secret" })).toThrow(/Linksammler-Payload/i);
|
||||
expect(() => validateCollectorInspectionRequest({ rawText: "x".repeat(2_000_001), addedAt: 1 })).toThrow(/Linksammler-Payload/i);
|
||||
expect(() => validateCollectorInspectionRequest({ rawText: "x", addedAt: -1 })).toThrow(/Linksammler-Payload/i);
|
||||
expect(() => validateCollectorInspectionRequest({ rawText: "x", addedAt: Number.MAX_SAFE_INTEGER + 1 })).toThrow(/Linksammler-Payload/i);
|
||||
});
|
||||
|
||||
it("accepts only bounded absolute collector container paths", () => {
|
||||
expect(validateCollectorContainerInspectionRequest(["C:\\Imports\\one.dlc", "D:\\two.dlc"], 1234)).toEqual({
|
||||
filePaths: ["C:\\Imports\\one.dlc", "D:\\two.dlc"],
|
||||
addedAt: 1234
|
||||
});
|
||||
expect(() => validateCollectorContainerInspectionRequest(["relative.dlc"], 1)).toThrow(/Container-Payload/i);
|
||||
expect(() => validateCollectorContainerInspectionRequest(["C:\\Imports\\one.txt"], 1)).toThrow(/Container-Payload/i);
|
||||
expect(() => validateCollectorContainerInspectionRequest(Array.from({ length: 101 }, (_unused, index) => `C:\\${index}.dlc`), 1)).toThrow(/Container-Payload/i);
|
||||
});
|
||||
|
||||
it("accepts IPC from the configured Vite development renderer origin", () => {
|
||||
expect(() => assertTrustedIpcSender(eventFor("http://localhost:5180/settings"), {
|
||||
isPackaged: false,
|
||||
|
||||
@@ -75,6 +75,7 @@ describe("global Escape selection routing", () => {
|
||||
expect(api.resolveEscapeSelectionScope?.("settings", "accounts", "INPUT", "text")).toBeNull();
|
||||
expect(api.resolveEscapeSelectionScope?.("downloads", "allgemein", "DIV")).toBe("downloads");
|
||||
expect(api.resolveEscapeSelectionScope?.("history", "accounts", "DIV")).toBe("history");
|
||||
expect(api.resolveEscapeSelectionScope?.("collector", "accounts", "DIV")).toBe("collector");
|
||||
});
|
||||
|
||||
it("releases the focused account row when Escape leaves account selection", () => {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"interactions": [
|
||||
{ "type": "click", "role": "button", "name": "Links hinzufügen" },
|
||||
{ "type": "fill", "role": "textbox", "name": "Links", "value": "https://example.test/a\nhttps://example.test/b" },
|
||||
{ "type": "click", "role": "button", "name": "Übernehmen" }
|
||||
{ "type": "click", "role": "button", "name": "Analysieren" }
|
||||
],
|
||||
"assertions": [
|
||||
{ "type": "active-view", "value": "collector" },
|
||||
@@ -228,7 +228,7 @@
|
||||
"interactions": [
|
||||
{ "type": "click", "role": "button", "name": "Links hinzufügen" },
|
||||
{ "type": "fill", "role": "textbox", "name": "Links", "value": "https://example.test/a\nhttps://example.test/b" },
|
||||
{ "type": "click", "role": "button", "name": "Übernehmen" }
|
||||
{ "type": "click", "role": "button", "name": "Analysieren" }
|
||||
],
|
||||
"assertions": [
|
||||
{ "type": "active-view", "value": "collector" },
|
||||
@@ -284,7 +284,7 @@
|
||||
"interactions": [
|
||||
{ "type": "click", "role": "button", "name": "Links hinzufügen" },
|
||||
{ "type": "fill", "role": "textbox", "name": "Links", "value": "https://example.test/a\nhttps://example.test/b" },
|
||||
{ "type": "click", "role": "button", "name": "Übernehmen" }
|
||||
{ "type": "click", "role": "button", "name": "Analysieren" }
|
||||
],
|
||||
"assertions": [
|
||||
{ "type": "active-view", "value": "collector" },
|
||||
@@ -300,7 +300,7 @@
|
||||
"interactions": [
|
||||
{ "type": "click", "role": "button", "name": "Links hinzufügen" },
|
||||
{ "type": "fill", "role": "textbox", "name": "Links", "value": "https://example.test/a\nhttps://example.test/b" },
|
||||
{ "type": "click", "role": "button", "name": "Übernehmen" }
|
||||
{ "type": "click", "role": "button", "name": "Analysieren" }
|
||||
],
|
||||
"assertions": [
|
||||
{ "type": "active-view", "value": "collector" },
|
||||
|
||||
@@ -9,18 +9,42 @@ import type {
|
||||
import { parseDebridLinkApiKeys } from "../../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../../src/shared/mega-debrid-accounts";
|
||||
import { createRendererState } from "../../src/main/renderer-state";
|
||||
import type { CollectorInspectionResult } from "../../src/shared/collector";
|
||||
|
||||
export const VISUAL_SCENARIOS = ["empty", "dense", "update"] as const;
|
||||
|
||||
export type VisualScenario = (typeof VISUAL_SCENARIOS)[number];
|
||||
|
||||
export interface VisualFixture {
|
||||
snapshot: UiSnapshot;
|
||||
export interface VisualFixture {
|
||||
snapshot: UiSnapshot;
|
||||
collector: CollectorInspectionResult;
|
||||
history: HistoryEntry[];
|
||||
update: UpdateCheckResult;
|
||||
traceConfig: SupportTraceConfig;
|
||||
remoteDiagnostics: RemoteDiagnosticsInfo;
|
||||
}
|
||||
}
|
||||
|
||||
function createCollectorInspection(): CollectorInspectionResult {
|
||||
return {
|
||||
invalidCount: 0,
|
||||
duplicateCount: 0,
|
||||
packages: [{
|
||||
id: "visual-collector-package-sbs14hd",
|
||||
name: "SBS14HD",
|
||||
addedAt: VISUAL_NOW_MS,
|
||||
links: Array.from({ length: 16 }, (_unused, index) => ({
|
||||
id: `visual-collector-link-${index + 1}`,
|
||||
url: `https://1fichier.com/?visual${String(index + 1).padStart(2, "0")}`,
|
||||
fileName: `SBS14HD.part${String(index + 1).padStart(2, "0")}.rar`,
|
||||
fileSizeBytes: index === 15 ? 373_517_856 : 471_859_200,
|
||||
hoster: "1fichier",
|
||||
availability: "online" as const,
|
||||
status: "ready" as const,
|
||||
addedAt: VISUAL_NOW_MS
|
||||
}))
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
export interface VisualClockTarget {
|
||||
setInterval: (handler: TimerHandler, timeout?: number, ...arguments_: unknown[]) => number;
|
||||
@@ -633,7 +657,8 @@ function createRemoteDiagnostics(): RemoteDiagnosticsInfo {
|
||||
export function createVisualFixture(scenario: VisualScenario): VisualFixture {
|
||||
if (scenario === "empty") {
|
||||
return {
|
||||
snapshot: createEmptySnapshot(),
|
||||
snapshot: createEmptySnapshot(),
|
||||
collector: createCollectorInspection(),
|
||||
history: [],
|
||||
update: createUpdate(false),
|
||||
traceConfig: createTraceConfig(),
|
||||
@@ -642,7 +667,8 @@ export function createVisualFixture(scenario: VisualScenario): VisualFixture {
|
||||
}
|
||||
|
||||
return {
|
||||
snapshot: createDenseSnapshot(),
|
||||
snapshot: createDenseSnapshot(),
|
||||
collector: createCollectorInspection(),
|
||||
history: createDenseHistory(),
|
||||
update: createUpdate(scenario === "update"),
|
||||
traceConfig: createTraceConfig(),
|
||||
|
||||
@@ -59,6 +59,8 @@ export function createVisualElectronApi(
|
||||
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),
|
||||
getStartConflicts: async () => [],
|
||||
resolveStartConflict: async (_packageId, policy) => ({
|
||||
skipped: policy === "skip",
|
||||
|
||||
Reference in New Issue
Block a user