feat: add per-provider account priority rules
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { DebridService, MEGA_DEBRID_STICKY_LINKS, primeMegaDebridRuntimeCooldownForTests, primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests, resetRealDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||
import { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||
import type { AppSettings } from "../src/shared/types";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
afterEach(() => { globalThis.fetch = originalFetch; vi.restoreAllMocks(); resetDebridLinkRuntimeStateForTests(); resetMegaDebridRuntimeStateForTests(); resetRealDebridRuntimeStateForTests(); });
|
||||
const json = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status, headers: { "Content-Type": "application/json" } });
|
||||
const result = { fileName: "fixture.rar", directUrl: "https://download.example.test/fixture.rar", fileSize: 1234, retriesUsed: 0 };
|
||||
const rdSettings = (): AppSettings => ({ ...defaultSettings(), token: "", realDebridApiTokens: serializeRealDebridApiAccounts([{ id: "rda_first", token: "first" }, { id: "rda_second", token: "second" }, { id: "rda_third", token: "third" }]), providerOrder: ["realdebrid"], providerPrimary: "realdebrid", autoProviderFallback: false, accountUsageRules: { realdebrid: { mode: "priority", accountIds: ["rda_second", "rda_first", "rda_third"] } } });
|
||||
|
||||
describe("fixed account priority routing", () => {
|
||||
it("reserves one Mega-Debrid conversion per account, waits when full and cancels waiting work", async () => {
|
||||
const first = getMegaDebridAccountId("first-slot");
|
||||
const second = getMegaDebridAccountId("second-slot");
|
||||
const settings: AppSettings = { ...defaultSettings(), megaDebridWebCredentials: "first-slot:pass-one\nsecond-slot:pass-two", megaDebridApiCredentials: "", megaDebridWebEnabled: true, megaDebridApiEnabled: false, providerOrder: ["megadebrid-web"], providerPrimary: "megadebrid-web", autoProviderFallback: false, accountUsageRules: { "megadebrid-web": { mode: "priority", accountIds: [second, first] } } };
|
||||
const completions: Array<() => void> = [];
|
||||
const web = vi.fn(() => new Promise<typeof result>((resolve) => completions.push(() => resolve(result))));
|
||||
const service = new DebridService(settings, { megaWebUnrestrict: web });
|
||||
const one = service.unrestrictLink("https://hoster.example/one.rar");
|
||||
const two = service.unrestrictLink("https://hoster.example/two.rar");
|
||||
const controller = new AbortController();
|
||||
const waiting = service.unrestrictLink("https://hoster.example/wait.rar", controller.signal);
|
||||
const cancelled = expect(waiting).rejects.toThrow(/aborted/i);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(web).toHaveBeenCalledTimes(2);
|
||||
controller.abort();
|
||||
await cancelled;
|
||||
const queued = service.unrestrictLink("https://hoster.example/queued.rar");
|
||||
completions[0]();
|
||||
expect((await one).sourceAccountId).toBe(second);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(web).toHaveBeenCalledTimes(3);
|
||||
completions[2]();
|
||||
expect((await queued).sourceAccountId).toBe(second);
|
||||
completions[1]();
|
||||
expect((await two).sourceAccountId).toBe(first);
|
||||
const next = service.unrestrictLink("https://hoster.example/next.rar");
|
||||
completions.at(-1)!();
|
||||
expect((await next).sourceAccountId).toBe(second);
|
||||
});
|
||||
it("applies an independent Mega-Debrid API order", async () => {
|
||||
const first = getMegaDebridAccountId("first-api");
|
||||
const second = getMegaDebridAccountId("second-api");
|
||||
const settings: AppSettings = { ...defaultSettings(), megaDebridApiCredentials: "first-api:pass-one\nsecond-api:pass-two", megaDebridWebCredentials: "", megaDebridApiEnabled: true, megaDebridWebEnabled: false, providerOrder: ["megadebrid-api"], providerPrimary: "megadebrid-api", autoProviderFallback: false, accountUsageRules: { "megadebrid-api": { mode: "priority", accountIds: [second, first] }, "megadebrid-web": { mode: "priority", accountIds: [first, second] } } };
|
||||
globalThis.fetch = vi.fn(async (input) => String(input).includes("action=connectUser")
|
||||
? json({ response_code: "ok", token: "fixture-api-session", vip_end: Math.floor(Date.now() / 1000) + 999999 })
|
||||
: json({ response_code: "ok", debridLink: result.directUrl, filename: result.fileName }));
|
||||
expect((await new DebridService(settings).unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe(second);
|
||||
});
|
||||
|
||||
it("can prioritize a Real-Debrid web account ahead of API accounts", async () => {
|
||||
const settings = rdSettings();
|
||||
settings.realDebridWebAccountIds = ["rdw_priority"];
|
||||
settings.realDebridUseWebLogin = true;
|
||||
settings.accountUsageRules.realdebrid!.accountIds = ["rdw_priority", "rda_second", "rda_first", "rda_third"];
|
||||
globalThis.fetch = vi.fn(async () => { throw new Error("API must not be used"); });
|
||||
const web = vi.fn(async () => result);
|
||||
expect((await new DebridService(settings, { realDebridWebUnrestrict: web }).unrestrictLink("https://hoster.example/web.rar")).sourceAccountId).toBe("rdw_priority");
|
||||
expect(web).toHaveBeenCalledTimes(1);
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([false, true])("exhausts the preferred provider's accounts before provider fallback=%s", async (autoProviderFallback) => {
|
||||
const settings = rdSettings();
|
||||
settings.providerOrder = ["realdebrid", "debridlink"];
|
||||
settings.debridLinkApiKeys = "fixture-fallback-key";
|
||||
settings.autoProviderFallback = autoProviderFallback;
|
||||
const used: string[] = [];
|
||||
globalThis.fetch = vi.fn(async (input, init) => {
|
||||
used.push(new Headers(init?.headers).get("Authorization")!);
|
||||
return String(input).includes("real-debrid") ? json({ error: "bad_token", error_code: 8 }, 401) : json({ success: true, value: { downloadUrl: result.directUrl, name: result.fileName, size: 1234 } });
|
||||
});
|
||||
const request = new DebridService(settings).unrestrictLink("https://hoster.example/file.rar");
|
||||
if (autoProviderFallback) expect((await request).provider).toBe("debridlink");
|
||||
else await expect(request).rejects.toThrow();
|
||||
expect(used).toEqual(["Bearer second", "Bearer first", "Bearer third", ...(autoProviderFallback ? ["Bearer fixture-fallback-key"] : [])]);
|
||||
});
|
||||
it("keeps Real-Debrid priority beyond sticky rotation and through parallel requests", async () => {
|
||||
const tokens: string[] = [];
|
||||
globalThis.fetch = vi.fn(async (_input, init) => {
|
||||
tokens.push(new Headers(init?.headers).get("Authorization")!);
|
||||
return json({ download: result.directUrl, filename: result.fileName, filesize: 1234 });
|
||||
});
|
||||
const service = new DebridService(rdSettings());
|
||||
for (let i = 0; i < 8; i++) expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe("rda_second");
|
||||
await Promise.all([1, 2, 3].map(() => service.unrestrictLink("https://hoster.example/file.rar")));
|
||||
expect(tokens).toEqual(Array(11).fill("Bearer second"));
|
||||
});
|
||||
|
||||
it("skips Real-Debrid cooldown, returns to the preferred account after expiry, and stops on link-wide errors", async () => {
|
||||
let fail = true;
|
||||
let offline = false;
|
||||
const used: string[] = [];
|
||||
globalThis.fetch = vi.fn(async (_input, init) => {
|
||||
const token = new Headers(init?.headers).get("Authorization")!;
|
||||
used.push(token);
|
||||
if (offline) return json({ error: "hoster_unavailable", error_code: 9 }, 503);
|
||||
if (fail && token === "Bearer second") return json({ error: "too_many_active_downloads", error_code: 20 }, 403);
|
||||
return json({ download: result.directUrl, filename: result.fileName, filesize: 1234 });
|
||||
});
|
||||
const service = new DebridService(rdSettings());
|
||||
expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe("rda_first");
|
||||
fail = false;
|
||||
expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe("rda_first");
|
||||
const now = Date.now();
|
||||
vi.spyOn(Date, "now").mockReturnValue(now + 2 * 60 * 60 * 1000);
|
||||
expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe("rda_second");
|
||||
offline = true;
|
||||
used.length = 0;
|
||||
await expect(service.unrestrictLink("https://hoster.example/offline.rar")).rejects.toThrow();
|
||||
expect(new Set(used)).toEqual(new Set(["Bearer second"]));
|
||||
});
|
||||
|
||||
it("skips disabled and daily-limited Real-Debrid accounts before calling the provider", async () => {
|
||||
const settings = rdSettings();
|
||||
settings.realDebridDisabledAccountIds = ["rda_second"];
|
||||
settings.realDebridAccountDailyLimitBytes = { rda_first: 100 };
|
||||
settings.realDebridAccountDailyUsageBytes = { rda_first: 100 };
|
||||
settings.providerDailyUsageDay = getProviderUsageDayKey();
|
||||
globalThis.fetch = vi.fn(async () => json({ download: result.directUrl, filename: result.fileName, filesize: 1234 }));
|
||||
expect((await new DebridService(settings).unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe("rda_third");
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps Mega-Debrid Web priority beyond the automatic rotation threshold and restores it after cooldown", async () => {
|
||||
const first = getMegaDebridAccountId("first");
|
||||
const second = getMegaDebridAccountId("second");
|
||||
const settings: AppSettings = { ...defaultSettings(), megaDebridWebCredentials: "first:pass-one\nsecond:pass-two", megaDebridApiCredentials: "", megaDebridWebEnabled: true, megaDebridApiEnabled: false, providerOrder: ["megadebrid-web"], providerPrimary: "megadebrid-web", autoProviderFallback: false, accountUsageRules: { "megadebrid-web": { mode: "priority", accountIds: [second, first] } } };
|
||||
const service = new DebridService(settings, { megaWebUnrestrict: vi.fn(async () => result) });
|
||||
for (let i = 0; i <= MEGA_DEBRID_STICKY_LINKS; i++) expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe(second);
|
||||
primeMegaDebridRuntimeCooldownForTests(`${second}:web`, 60000);
|
||||
expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe(first);
|
||||
const now = Date.now();
|
||||
vi.spyOn(Date, "now").mockReturnValue(now + 120000);
|
||||
expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe(second);
|
||||
});
|
||||
|
||||
it("uses Debrid-Link key priority, skips cooldown, and returns to the first usable key", async () => {
|
||||
const debridLinkApiKeys = "fixture-key-one\nfixture-key-two\nfixture-key-three";
|
||||
const keys = parseDebridLinkApiKeys(debridLinkApiKeys);
|
||||
const settings: AppSettings = { ...defaultSettings(), debridLinkApiKeys, providerOrder: ["debridlink"], providerPrimary: "debridlink", autoProviderFallback: false, accountUsageRules: { debridlink: { mode: "priority", accountIds: [keys[2].id, keys[1].id, keys[0].id] } } };
|
||||
globalThis.fetch = vi.fn(async () => json({ success: true, value: { downloadUrl: result.directUrl, name: result.fileName, size: 1234 } }));
|
||||
const service = new DebridService(settings);
|
||||
expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe(keys[2].id);
|
||||
primeDebridLinkRuntimeCooldownForTests(keys[2].id, 60000);
|
||||
expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe(keys[1].id);
|
||||
const now = Date.now();
|
||||
vi.spyOn(Date, "now").mockReturnValue(now + 120000);
|
||||
expect((await service.unrestrictLink("https://hoster.example/file.rar")).sourceAccountId).toBe(keys[2].id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { configureCredentialProtector } from "../src/main/credential-protection";
|
||||
import { defaultSettings } from "../src/main/constants";
|
||||
import { applyAccountCommand } from "../src/main/account-commands";
|
||||
import { createRendererSettings } from "../src/main/renderer-state";
|
||||
import { validateRendererSettingsUpdate } from "../src/main/renderer-settings";
|
||||
import { createStoragePaths, loadSettings, normalizeSettings, saveSettings } from "../src/main/storage";
|
||||
import { normalizeAccountUsageRules, orderAccountsByRule, reconcileAccountOrder } from "../src/shared/account-usage-rules";
|
||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||
import type { AccountRuleProvider } from "../src/shared/account-usage-rules";
|
||||
import type { RendererAccountKind } from "../src/shared/types";
|
||||
|
||||
const directories: string[] = [];
|
||||
beforeEach(() => configureCredentialProtector({ isEncryptionAvailable: () => true, encryptString: (value) => Buffer.from(value).reverse(), decryptString: (value) => Buffer.from(value).reverse().toString() }));
|
||||
afterEach(() => { for (const directory of directories.splice(0)) fs.rmSync(directory, { recursive: true, force: true }); });
|
||||
|
||||
describe("account usage priorities", () => {
|
||||
it("preserves automatic defaults and filters corrupt configuration", () => {
|
||||
expect(defaultSettings().accountUsageRules).toEqual({});
|
||||
expect(normalizeSettings({ ...defaultSettings(), accountUsageRules: undefined as never }).accountUsageRules).toEqual({});
|
||||
expect(normalizeAccountUsageRules({ wrong: {}, realdebrid: { mode: "bad", accountIds: ["b", "b", 4, ""] } })).toEqual({ realdebrid: { mode: "automatic", accountIds: ["b"] } });
|
||||
});
|
||||
|
||||
it("restores known IDs, removes stale and duplicate IDs, and appends new accounts", () => {
|
||||
expect(reconcileAccountOrder(["b", "gone", "b", "a"], ["a", "b", "new"])).toEqual(["b", "a", "new"]);
|
||||
const accounts = [{ id: "a" }, { id: "b" }, { id: "new" }];
|
||||
expect(orderAccountsByRule(accounts, { mode: "priority", accountIds: ["b", "a"] }).map((a) => a.id)).toEqual(["b", "a", "new"]);
|
||||
expect(orderAccountsByRule(accounts, { mode: "automatic", accountIds: ["b", "a"] })).toEqual(accounts);
|
||||
});
|
||||
|
||||
it("validates nested rules without allowing arbitrary providers or malformed IDs", () => {
|
||||
const current = defaultSettings();
|
||||
for (const rule of [{ mode: "random", accountIds: [] }, { mode: "priority", accountIds: [3] }, { mode: "priority", accountIds: [], secret: "no" }]) {
|
||||
expect(() => validateRendererSettingsUpdate({ accountUsageRules: { realdebrid: rule } }, current)).toThrow();
|
||||
}
|
||||
expect(() => validateRendererSettingsUpdate({ accountUsageRules: { unknown: { mode: "priority", accountIds: [] } } }, current)).toThrow();
|
||||
const accountUsageRules = { realdebrid: { mode: "priority" as const, accountIds: ["a"] } };
|
||||
expect(validateRendererSettingsUpdate({ accountUsageRules }, current)).toEqual({ accountUsageRules });
|
||||
const projected = createRendererSettings({ ...current, accountUsageRules });
|
||||
projected.accountUsageRules.realdebrid!.accountIds.push("b");
|
||||
expect(accountUsageRules.realdebrid.accountIds).toEqual(["a"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["debridlink-api", "debridlink"], ["megadebrid-api", "megadebrid-api"], ["megadebrid-web", "megadebrid-web"], ["realdebrid-api", "realdebrid"]
|
||||
] as [RendererAccountKind, AccountRuleProvider][])("preserves position across replace, append, delete and reload for %s", (kind, provider) => {
|
||||
let settings = defaultSettings();
|
||||
const add = (identity: string, secret: string) => {
|
||||
const result = applyAccountCommand(settings, { action: "create", kind, identity, secret });
|
||||
settings = result.settings;
|
||||
return result.response.accountId!;
|
||||
};
|
||||
const first = add("first@example.test", "fixture-first-key");
|
||||
const second = add("second@example.test", "fixture-second-key");
|
||||
settings.accountUsageRules = { [provider]: { mode: "priority", accountIds: [second, first] } };
|
||||
const replacement = applyAccountCommand(settings, { action: "replace", kind, accountId: second, identity: "renamed@example.test", secret: "fixture-renamed-key" });
|
||||
settings = replacement.settings;
|
||||
const replacementId = replacement.response.accountId!;
|
||||
expect(settings.accountUsageRules[provider]?.accountIds).toEqual([replacementId, first]);
|
||||
const third = add("third@example.test", "fixture-third-key");
|
||||
expect(settings.accountUsageRules[provider]?.accountIds).toEqual([replacementId, first, third]);
|
||||
settings = applyAccountCommand(settings, { action: "delete", kind, accountId: first }).settings;
|
||||
expect(settings.accountUsageRules[provider]?.accountIds).toEqual([replacementId, third]);
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-account-priority-"));
|
||||
directories.push(directory);
|
||||
const paths = createStoragePaths(directory);
|
||||
saveSettings(paths, settings);
|
||||
expect(loadSettings(paths).accountUsageRules).toEqual(settings.accountUsageRules);
|
||||
});
|
||||
|
||||
it("prunes references to accounts missing from imported configuration", () => {
|
||||
const debridLinkApiKeys = "fixture-key-one\nfixture-key-two";
|
||||
const [first, second] = parseDebridLinkApiKeys(debridLinkApiKeys);
|
||||
const settings = normalizeSettings({ ...defaultSettings(), debridLinkApiKeys, accountUsageRules: { debridlink: { mode: "priority", accountIds: ["deleted", second.id] } } });
|
||||
expect(settings.accountUsageRules.debridlink?.accountIds).toEqual([second.id, first.id]);
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,13 @@ const statistics: StatisticsLedger = { version: 2, startedAt: 1, days: [], minut
|
||||
const baseInput = { appVersion: "1.7.183", exportedAt: "2026-06-07T00:00:00Z", session, history, statistics };
|
||||
|
||||
describe("buildBackupPayload — default is settings-only", () => {
|
||||
it("keeps account selection mode and priority IDs through encrypted export and import", () => {
|
||||
const accountUsageRules = { debridlink: { mode: "priority" as const, accountIds: ["second", "first"] } };
|
||||
const payload = buildBackupPayload({ ...baseInput, settings: settings({ accountUsageRules }) });
|
||||
const restored = JSON.parse(decryptBackup(encryptBackup(JSON.stringify(payload), "fixture-priority-passphrase"), "fixture-priority-passphrase"));
|
||||
expect(planBackupImport(restored).valid).toBe(true);
|
||||
expect(restored.settings.accountUsageRules).toEqual(accountUsageRules);
|
||||
});
|
||||
it("keeps the semantic theme preference in exported settings", () => {
|
||||
const payload = buildBackupPayload({
|
||||
...baseInput,
|
||||
|
||||
@@ -2,6 +2,12 @@ import { describe, expect, it } from "vitest";
|
||||
import { normalizeLanguage, translateUiText } from "../src/renderer/i18n";
|
||||
|
||||
describe("renderer localization", () => {
|
||||
it("translates account priority controls", () => {
|
||||
expect(translateUiText("Account-Reihenfolge", "en")).toBe("Account order");
|
||||
expect(translateUiText("Automatisch verteilen", "en")).toBe("Automatic distribution");
|
||||
expect(translateUiText("Feste Reihenfolge", "en")).toBe("Fixed priority");
|
||||
expect(translateUiText("Fixed priority", "de")).toBe("Feste Reihenfolge");
|
||||
});
|
||||
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");
|
||||
|
||||
@@ -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>Account-Prioritätsprüfung</title></head>
|
||||
<body><div id="root"></div><script type="module" src="/account-priority.tsx"></script></body>
|
||||
</html>
|
||||
@@ -0,0 +1,67 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "../../src/renderer/App";
|
||||
import { createVisualFixture } from "./fixtures";
|
||||
import { createVisualElectronApi } from "./mock-electron-api";
|
||||
import type { RendererSettingsUpdate } from "../../src/shared/types";
|
||||
import "../../src/renderer/theme.css";
|
||||
import "../../src/renderer/styles.css";
|
||||
|
||||
const fixture = createVisualFixture("dense");
|
||||
fixture.snapshot.settings.language = "de";
|
||||
fixture.snapshot.settings.theme = new URLSearchParams(location.search).get("theme") === "light" ? "light" : "dark";
|
||||
fixture.snapshot.settings.themePreference = fixture.snapshot.settings.theme;
|
||||
const baseApi = createVisualElectronApi(fixture);
|
||||
let saved: RendererSettingsUpdate | undefined;
|
||||
window.rd = { ...baseApi, updateSettings: async (settings) => { saved = structuredClone(settings); return baseApi.updateSettings(settings); } };
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
const report = document.createElement("output");
|
||||
document.body.append(report);
|
||||
const waitFor = async (condition: () => boolean): Promise<void> => {
|
||||
const deadline = Date.now() + 10000;
|
||||
while (!condition()) {
|
||||
if (Date.now() > deadline) throw new Error("Account priority check timed out");
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
};
|
||||
const click = (selector: string): void => document.querySelector<HTMLElement>(selector)!.click();
|
||||
const assert = (condition: boolean, message: string): void => { if (!condition) throw new Error(message); };
|
||||
|
||||
async function verify(): Promise<void> {
|
||||
await waitFor(() => Boolean(document.querySelector('[data-main-view="settings"]')));
|
||||
click('[data-main-view="settings"]');
|
||||
await waitFor(() => Boolean(document.querySelector(".settings-sidebar-item")));
|
||||
[...document.querySelectorAll<HTMLButtonElement>(".settings-sidebar-item")].find((button) => button.textContent === "Accounts")!.click();
|
||||
await waitFor(() => Boolean(document.querySelector("#settings-account-rules-tab")));
|
||||
click("#settings-account-rules-tab");
|
||||
await waitFor(() => Boolean(document.querySelector('[name="account-selection-debridlink"]')));
|
||||
const control = document.querySelector<HTMLInputElement>('[name="account-selection-debridlink"]')!.closest<HTMLDetailsElement>("details")!;
|
||||
control.open = true;
|
||||
const ids = () => [...control.querySelectorAll<HTMLElement>("[data-priority-account]")].map((row) => row.dataset.priorityAccount!);
|
||||
const original = ids();
|
||||
assert(original.length === 2, "Fixture needs two keys");
|
||||
const radios = control.querySelectorAll<HTMLInputElement>('input[type="radio"]');
|
||||
assert(radios[0].checked, "Automatic mode must be the default");
|
||||
radios[1].click();
|
||||
await waitFor(() => radios[1].checked);
|
||||
control.querySelectorAll<HTMLButtonElement>('[data-priority-account] button')[1].click();
|
||||
await waitFor(() => ids()[0] === original[1]);
|
||||
const transfer = new DataTransfer();
|
||||
const rows = control.querySelectorAll<HTMLElement>("[data-priority-account]");
|
||||
rows[0].dispatchEvent(new DragEvent("dragstart", { bubbles: true, dataTransfer: transfer }));
|
||||
rows[1].dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer: transfer }));
|
||||
await waitFor(() => ids()[0] === original[0]);
|
||||
const crossProvider = new DataTransfer();
|
||||
crossProvider.setData("application/x-mdd-account-priority", JSON.stringify({ provider: "realdebrid", id: original[1] }));
|
||||
rows[0].dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer: crossProvider }));
|
||||
assert(ids()[0] === original[0], "Cross-provider drag changed the account order");
|
||||
control.querySelectorAll<HTMLButtonElement>('[data-priority-account] button')[1].click();
|
||||
await waitFor(() => ids()[0] === original[1]);
|
||||
[...document.querySelectorAll<HTMLButtonElement>("button")].find((button) => button.textContent === "Einstellungen speichern")!.click();
|
||||
await waitFor(() => Boolean(saved?.accountUsageRules?.debridlink));
|
||||
assert(saved!.accountUsageRules!.debridlink!.mode === "priority", "Mode was not saved");
|
||||
assert(saved!.accountUsageRules!.debridlink!.accountIds.join() === [...original].reverse().join(), "Order was not saved");
|
||||
assert(control.scrollWidth <= control.clientWidth + 1, "Priority controls overflow horizontally");
|
||||
control.scrollIntoView({ block: "center" });
|
||||
report.textContent = JSON.stringify({ passed: true, theme: fixture.snapshot.settings.theme, checks: ["expand", "automatic default", "mode change", "arrow reorder", "drag reorder", "reject cross-provider drag", "save mode and order", "no horizontal overflow"] });
|
||||
}
|
||||
void verify().catch((error) => { report.textContent = String(error); });
|
||||
@@ -86,7 +86,8 @@ function createSettings(): AppSettings {
|
||||
linkSnappyPassword: "visual-password",
|
||||
archivePasswordList: "visual-archive-password",
|
||||
rememberToken: true,
|
||||
providerOrder: ["realdebrid", "megadebrid-api", "bestdebrid", "alldebrid", "debridlink"],
|
||||
providerOrder: ["realdebrid", "megadebrid-api", "bestdebrid", "alldebrid", "debridlink"],
|
||||
accountUsageRules: {},
|
||||
providerPrimary: "realdebrid",
|
||||
providerSecondary: "megadebrid-api",
|
||||
providerTertiary: "bestdebrid",
|
||||
|
||||
Reference in New Issue
Block a user