fix: make clipboard collector navigation opt-in
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldFocusCollectorImport } from "../src/renderer/collector-navigation";
|
||||
|
||||
describe("collector import navigation", () => {
|
||||
it("keeps automatic clipboard imports in the background unless explicitly enabled", () => {
|
||||
expect(shouldFocusCollectorImport("clipboard", undefined)).toBe(false);
|
||||
expect(shouldFocusCollectorImport("clipboard", false)).toBe(false);
|
||||
expect(shouldFocusCollectorImport("clipboard", true)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps explicit manual imports focused regardless of the automatic setting", () => {
|
||||
for (const enabled of [undefined, false, true]) {
|
||||
expect(shouldFocusCollectorImport("manual", enabled)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,11 @@ import { describe, expect, it } from "vitest";
|
||||
import { normalizeLanguage, translateUiText } from "../src/renderer/i18n";
|
||||
|
||||
describe("renderer localization", () => {
|
||||
it("translates the optional clipboard navigation setting", () => {
|
||||
expect(translateUiText("Bei erkannten Zwischenablage-Links zum Linksammler wechseln", "en")).toBe("Switch to Link Collector when clipboard links are detected");
|
||||
expect(translateUiText("Switch to Link Collector when clipboard links are detected", "de")).toBe("Bei erkannten Zwischenablage-Links zum Linksammler wechseln");
|
||||
expect(translateUiText("Standardmäßig aus: Links werden im Hintergrund gesammelt, ohne den aktuellen Tab zu wechseln. Gilt nur bei aktivierter Zwischenablage-Überwachung.", "en")).toBe("Off by default: links are collected in the background without switching tabs. Applies only when clipboard monitoring is enabled.");
|
||||
});
|
||||
it("falls back to English", () => {
|
||||
expect(normalizeLanguage(undefined)).toBe("en");
|
||||
expect(normalizeLanguage("fr")).toBe("en");
|
||||
|
||||
@@ -4,6 +4,15 @@ import { createRendererSettings } from "../src/main/renderer-state";
|
||||
import { validateRendererSettingsUpdate } from "../src/main/renderer-settings";
|
||||
|
||||
describe("renderer settings validation", () => {
|
||||
it("projects and validates the optional clipboard tab switch as a boolean", () => {
|
||||
const current = defaultSettings();
|
||||
expect(createRendererSettings(current).switchToCollectorOnClipboard).toBe(false);
|
||||
for (const enabled of [true, false]) {
|
||||
expect(validateRendererSettingsUpdate({ switchToCollectorOnClipboard: enabled }, current)).toEqual({ switchToCollectorOnClipboard: enabled });
|
||||
expect(createRendererSettings({ ...current, switchToCollectorOnClipboard: enabled }).switchToCollectorOnClipboard).toBe(enabled);
|
||||
}
|
||||
expect(() => validateRendererSettingsUpdate({ switchToCollectorOnClipboard: "true" }, current)).toThrow("Settings-Payload ist ungültig");
|
||||
});
|
||||
it("projects and validates startup work-directory creation", () => {
|
||||
const current = { ...defaultSettings(), createWorkDirectoriesOnStartup: true };
|
||||
const projected = createRendererSettings(current);
|
||||
|
||||
@@ -589,6 +589,8 @@ describe("settings views", () => {
|
||||
scheduleSpeedInputs: {}
|
||||
});
|
||||
const language = form.groups.flatMap((group) => group.fields).find((field) => field.id === "language");
|
||||
const clipboardSwitch = form.groups.flatMap((group) => group.fields).find((field) => field.id === "switchToCollectorOnClipboard");
|
||||
expect(clipboardSwitch).toMatchObject({ kind: "switch", value: false, label: "Bei erkannten Zwischenablage-Links zum Linksammler wechseln" });
|
||||
|
||||
expect(language).toEqual({
|
||||
id: "language",
|
||||
|
||||
@@ -12,6 +12,23 @@ import { configureCredentialProtector } from "../src/main/credential-protection"
|
||||
import { acquirePersistenceBarrier, addHistoryEntryForRetention, clearHistory, createStoragePaths, emptySession, loadHistory, loadHistoryForRetention, loadSession, loadSessionWithStatus, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeSettings, removeHistoryEntries, replaceHistory, resetHistoryForRetention, saveHistory, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "../src/main/storage";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
describe("automatic collector navigation settings", () => {
|
||||
it("defaults new and existing installations to background collection", () => {
|
||||
expect(defaultSettings().switchToCollectorOnClipboard).toBe(false);
|
||||
expect(loadSettingsFrom({ clipboardWatch: true }).switchToCollectorOnClipboard).toBe(false);
|
||||
expect(loadSettingsFrom({ switchToCollectorOnClipboard: "false" }).switchToCollectorOnClipboard).toBe(false);
|
||||
});
|
||||
|
||||
it("persists opt-in and subsequent opt-out across reloads", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-collector-navigation-"));
|
||||
tempDirs.push(dir);
|
||||
const paths = createStoragePaths(dir);
|
||||
for (const enabled of [true, false]) {
|
||||
saveSettings(paths, { ...defaultSettings(), switchToCollectorOnClipboard: enabled });
|
||||
expect(loadSettings(paths).switchToCollectorOnClipboard).toBe(enabled);
|
||||
}
|
||||
});
|
||||
});
|
||||
type SettingsSaveMode = "sync" | "async";
|
||||
|
||||
async function saveSettingsInMode(mode: SettingsSaveMode, paths: ReturnType<typeof createStoragePaths>, settings: AppSettings): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Linksammler-Navigationsprüfung</title></head>
|
||||
<body><div id="root"></div><script type="module" src="/collector-navigation.tsx"></script></body>
|
||||
</html>
|
||||
@@ -0,0 +1,81 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "../../src/renderer/App";
|
||||
import type { ElectronApi } from "../../src/shared/preload-api";
|
||||
import type { CollectorPersistenceState } from "../../src/shared/collector";
|
||||
import { createVisualFixture } from "./fixtures";
|
||||
import { createVisualElectronApi } from "./mock-electron-api";
|
||||
import "../../src/renderer/theme.css";
|
||||
import "../../src/renderer/styles.css";
|
||||
|
||||
const fixture = createVisualFixture("dense");
|
||||
fixture.snapshot.settings.switchToCollectorOnClipboard = false;
|
||||
let clipboardListener: ((links: string[]) => void) | undefined;
|
||||
const stateListeners = new Set<Parameters<ElectronApi["onStateUpdate"]>[0]>();
|
||||
let saved: CollectorPersistenceState = { packages: [], collapsedPackageIds: [] };
|
||||
let preparedCount = 0;
|
||||
let releasePreparation: (() => void) | undefined;
|
||||
let delayPreparation = false;
|
||||
window.rd = {
|
||||
...createVisualElectronApi(fixture),
|
||||
onClipboardDetected: (listener) => { clipboardListener = listener; return () => { clipboardListener = undefined; }; },
|
||||
onStateUpdate: (listener) => { stateListeners.add(listener); return () => { stateListeners.delete(listener); }; },
|
||||
updateSettings: async (settings) => {
|
||||
Object.assign(fixture.snapshot.settings, settings);
|
||||
fixture.snapshot.snapshotRevision = (fixture.snapshot.snapshotRevision ?? 0) + 1;
|
||||
for (const listener of stateListeners) listener(structuredClone(fixture.snapshot));
|
||||
return structuredClone(fixture.snapshot.settings);
|
||||
},
|
||||
prepareCollectorText: async ({ rawText, addedAt }) => {
|
||||
const id = `navigation-${++preparedCount}`;
|
||||
if (delayPreparation) await new Promise<void>((resolve) => { releasePreparation = resolve; });
|
||||
return {
|
||||
packages: [{ id, name: id, nameSource: "explicit", addedAt, links: [{ id: `${id}-link`, url: rawText, fileName: `${id}.rar`, fileSizeBytes: null, hoster: "example.test", availability: "unknown", status: "unknown", addedAt }] }],
|
||||
invalidCount: 0, duplicateCount: 0
|
||||
};
|
||||
},
|
||||
saveCollectorState: async (state) => { saved = structuredClone(state); return structuredClone(state); }
|
||||
};
|
||||
|
||||
const report = document.createElement("output");
|
||||
document.body.append(report);
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
const waitFor = async (condition: () => boolean): Promise<void> => {
|
||||
const deadline = Date.now() + 10000;
|
||||
while (!condition()) {
|
||||
if (Date.now() > deadline) throw new Error("Navigation check timed out");
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
};
|
||||
const activeView = (): string | undefined => document.querySelector<HTMLElement>("[data-visual-active-view]")?.dataset.visualActiveView;
|
||||
const navigate = async (view: string): Promise<void> => {
|
||||
document.querySelector<HTMLButtonElement>(`[data-main-view="${view}"]`)!.click();
|
||||
await waitFor(() => activeView() === view);
|
||||
};
|
||||
const check = (condition: boolean, message: string): void => { if (!condition) throw new Error(message); };
|
||||
|
||||
async function verify(): Promise<void> {
|
||||
await waitFor(() => Boolean(clipboardListener) && activeView() === "downloads");
|
||||
clipboardListener!(["https://example.test/background.rar"]);
|
||||
await waitFor(() => saved.packages.length === 1);
|
||||
check(activeView() === "downloads", "Default changed the downloads tab");
|
||||
await navigate("settings");
|
||||
clipboardListener!(["https://example.test/settings.rar"]);
|
||||
await waitFor(() => saved.packages.length === 2);
|
||||
check(activeView() === "settings", "Background import interrupted settings");
|
||||
await window.rd.updateSettings({ switchToCollectorOnClipboard: true });
|
||||
await new Promise((resolve) => setTimeout(resolve, 900));
|
||||
clipboardListener!(["https://example.test/enabled.rar"]);
|
||||
await waitFor(() => saved.packages.length === 3 && activeView() === "collector");
|
||||
await navigate("downloads");
|
||||
delayPreparation = true;
|
||||
clipboardListener!(["https://example.test/disabled-during-import.rar"]);
|
||||
await waitFor(() => Boolean(releasePreparation));
|
||||
await window.rd.updateSettings({ switchToCollectorOnClipboard: false });
|
||||
await new Promise((resolve) => setTimeout(resolve, 900));
|
||||
releasePreparation!();
|
||||
await waitFor(() => saved.packages.length === 4);
|
||||
check(activeView() === "downloads", "An in-flight import ignored disabling the setting");
|
||||
report.textContent = JSON.stringify({ passed: true, checks: ["default keeps downloads", "background preserves settings", "opt-in opens collector", "opt-out during import prevents switch"], savedPackages: saved.packages.length });
|
||||
}
|
||||
|
||||
void verify().catch((error) => { report.textContent = String(error); });
|
||||
@@ -126,6 +126,7 @@ function createSettings(): AppSettings {
|
||||
updateRepo: "Sucukdeluxe/Multi-Debrid-Downloader",
|
||||
autoUpdateCheck: true,
|
||||
clipboardWatch: true,
|
||||
switchToCollectorOnClipboard: false,
|
||||
minimizeToTray: false,
|
||||
theme: "dark",
|
||||
themePreference: "dark",
|
||||
|
||||
Reference in New Issue
Block a user