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 { defaultSettings } from "../src/main/constants"; import { createRendererSettings, createRendererState } from "../src/main/renderer-state"; import { buildAccountAddFields, createAccountDialogState, createSettingsDraft } from "../src/renderer/App"; import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit"; import { buildScopedAccountEnabledState, buildConfiguredProviderOrder, resolveAccountStatusState, runOptimisticAccountUpdate } from "../src/renderer/account-ui"; import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys"; import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts"; import { ACCOUNT_COLUMNS, SETTINGS_SECTIONS, buildSettingsFormViewModel, buildAccountRowId, buildTargetedAccountCheck, filterAccountAddOptions, getSettingsSaveLabel, getSettingsSelectNavigationIndex, normalizeNotificationNumberField, projectAccountRows, pruneAccountSelection, reconcileAccountAddDraft, resolveHistoryRetentionSelection, sortAccountRows, type AccountAddOption, type AccountRowSource, type SettingsFormViewModel } from "../src/renderer/views/settings/settings-model"; import * as settingsModel from "../src/renderer/views/settings/settings-model"; import { AccountAddDialog, AccountEditDialog, AccountWorkspace, type AccountWorkspaceActions, type AccountWorkspaceViewModel } from "../src/renderer/views/settings/AccountWorkspace"; import { SettingsForm, closeSettingsSelectAndRestoreFocus, getSettingsSelectKeyboardAction } from "../src/renderer/views/settings/SettingsForm"; import { SettingsContent, SettingsSidebar, SettingsView, type SettingsViewActions, type SettingsViewModel } from "../src/renderer/views/settings/SettingsView"; const GIB = 1024 * 1024 * 1024; const NOW = 1_700_000_000_000; const appSource = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8"); const accountWorkspaceSource = readFileSync( new URL("../src/renderer/views/settings/AccountWorkspace.tsx", import.meta.url), "utf8" ); const settingsCss = readFileSync( new URL("../src/renderer/views/settings/settings.css", import.meta.url), "utf8" ); function sourceBlock(source: string, start: string, end: string): string { return source.slice(source.indexOf(start), source.indexOf(end, source.indexOf(start))); } 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 findElements(node: ReactNode, predicate: (element: ReactElement) => boolean): ReactElement[] { const results: ReactElement[] = []; visitElements(node, (element) => { if (predicate(element)) { results.push(element); } }); return results; } function compositeKeyboardTarget(count: number): { elements: Array<{ closest: () => { querySelectorAll: () => unknown[] }; focus: () => void }>; focused: () => number; } { let focusedIndex = -1; const elements: Array<{ closest: () => { querySelectorAll: () => unknown[] }; focus: () => void }> = []; const container = { querySelectorAll: () => elements }; for (let index = 0; index < count; index += 1) { elements.push({ closest: () => container, focus: () => { focusedIndex = index; } }); } return { elements, focused: () => focusedIndex }; } function count(haystack: string, needle: string): number { return haystack.split(needle).length - 1; } function accountSources(): AccountRowSource[] { return [ { identityId: "mega-premium", service: "megadebrid-api", hoster: "Mega-Debrid", mode: "API", icon: "./provider-icons/mega-debrid.png", enabled: true, status: { state: "premium", message: "Premium aktiv", premiumUntilMs: NOW + 7 * 24 * 60 * 60 * 1000, checkedAt: NOW - 5 * 60 * 1000, email: "verified@example.test" }, dailyLimitBytes: 10 * GIB, dailyUsageBytes: 4 * GIB, username: "stored-user", credentialKind: "password", canCheck: true }, { identityId: "debrid-free", service: "debridlink", hoster: "Debrid-Link", mode: "API-Key", icon: "./provider-icons/debrid-link.ico", enabled: true, status: { state: "free", message: "Free Account", premiumUntilMs: null, username: "xsucukde5" }, dailyLimitBytes: 0, dailyUsageBytes: 0, username: "", credentialKind: "api-key", canCheck: true }, { identityId: "invalid", service: "ddownload", hoster: "DDownload", mode: "Login", icon: "./provider-icons/ddownload.ico", enabled: true, status: { state: "invalid", message: "Login abgelehnt", premiumUntilMs: null }, username: "invalid@example.test", credentialKind: "password", canCheck: false }, { identityId: "unknown", service: "onefichier", hoster: "1Fichier", mode: "API", icon: "./provider-icons/onefichier.png", enabled: true, status: { state: "unchecked", message: "", premiumUntilMs: null }, username: "—", credentialKind: "api-key", canCheck: false }, { identityId: "disabled", service: "linksnappy", hoster: "LinkSnappy", mode: "Web-Login", icon: "./provider-icons/linksnappy.png", enabled: false, status: { state: "disabled", message: "", premiumUntilMs: null }, username: "disabled@example.test", credentialKind: "password", canCheck: false } ]; } function accountOptions(): AccountAddOption[] { return [ { id: "realdebrid-api", service: "realdebrid", title: "Real-Debrid", mode: "API", description: "API-Token verwenden", functionLabel: "API-Token", filter: "api", multi: false }, { id: "ddownload-login", service: "ddownload", title: "DDownload", mode: "Login", description: "Login und Passwort", functionLabel: "Login:Passwort", filter: "web", multi: false }, { id: "megadebrid-api", service: "megadebrid-api", title: "Mega-Debrid", mode: "API", description: "Weiteren Account hinzufügen", functionLabel: "Login:Passwort", filter: "api", multi: true }, { id: "debridlink-api", service: "debridlink", title: "Debrid-Link", mode: "API", description: "Weiteren API-Key hinzufügen", functionLabel: "API-Key", filter: "api", multi: true } ]; } function formModel(): SettingsFormViewModel { return { title: "Allgemein", description: "Grundeinstellungen der Anwendung.", groups: [ { id: "appearance", title: "Darstellung", fields: [ { id: "downloadDir", kind: "path", label: "Download-Ordner", value: "C:\\Downloads", help: "Zielordner für Downloads." }, { id: "theme", kind: "theme", label: "Theme", value: "dark", options: [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, { value: "system", label: "System" } ] }, { id: "autoUpdate", kind: "switch", label: "Automatisch nach Updates suchen", value: true } ] } ] }; } function workspaceModel(): AccountWorkspaceViewModel { return { activePanel: "overview", rows: projectAccountRows(accountSources(), [buildAccountRowId("megadebrid-api", "API", "mega-premium")], NOW), selectedIds: [buildAccountRowId("megadebrid-api", "API", "mega-premium")], busy: false, rules: { providerOrder: ["Debrid-Link", "Real-Debrid"], routing: ["rapidgator.net → Debrid-Link"], autoFallback: true }, runtime: { providers: [ { id: "realdebrid", label: "Real-Debrid", accountCount: 2, availableAccountCount: 1, activeDownloads: 3, dailyUsageText: "12,5 GB" } ], accounts: [ { id: "rd-runtime", providerLabel: "Real-Debrid", modeLabel: "API", identity: "stored-user", stateLabel: "Cooldown", stateTone: "warning", activeDownloads: 0, dailyUsageText: "4,2 GB", successRateText: "75 % (3/4)", lastUsedText: "vor 2 Min.", cooldownText: "Rate-Limit aktiv · 48 Sek." } ] } }; } function workspaceActions(overrides: Partial = {}): AccountWorkspaceActions { return { onPanelChange: () => {}, onSelect: () => {}, onToggleEnabled: () => {}, onEdit: () => {}, onContextMenu: () => {}, onCopyIdentity: () => {}, onAdd: () => {}, onRemoveSelected: () => {}, onCheckActive: () => {}, onCheckAll: () => {}, ...overrides }; } function viewModel(saveState: SettingsViewModel["saveState"] = "clean"): SettingsViewModel { return { section: "accounts", saveState, form: formModel(), accounts: workspaceModel() }; } function viewActions(): SettingsViewActions { return { onSectionChange: () => {}, onSave: () => {}, form: { onChange: () => {}, onAction: () => {} }, accounts: workspaceActions() }; } describe("settings model", () => { it("keeps six stable sections and accessible save-state labels", () => { expect(SETTINGS_SECTIONS).toEqual([ { id: "allgemein", label: "Allgemein" }, { id: "accounts", label: "Accounts" }, { id: "extract", label: "Entpacken" }, { id: "speed", label: "Geschwindigkeit" }, { id: "cleanup", label: "Bereinigung" }, { id: "updates", label: "Updates" } ]); expect(["clean", "dirty", "saving", "saved", "error"].map((state) => getSettingsSaveLabel(state as never))) .toEqual(["Gespeichert", "Ungespeicherte Änderungen", "Wird gespeichert…", "Gespeichert", "Speichern fehlgeschlagen"]); }); it("projects stable sanitized rows with full verified usernames and distinct states", () => { const rows = projectAccountRows(accountSources(), [], NOW); expect(rows.map((row) => row.id)).toEqual(accountSources().map((source) => buildAccountRowId(source.service, source.mode, source.identityId))); expect(rows[0].username).toBe("stored-user"); expect(rows[0].email).toBe("verified@example.test"); expect(rows[0].credential).toBe("••••••"); expect(rows[1].username).toBe("xsucukde5"); expect(rows[1].email).toBe("—"); expect(rows[1].credential).toBe("API-Key"); expect(rows.map((row) => row.status.tone)).toEqual(["ok", "free", "invalid", "unknown", "disabled"]); expect(rows.map((row) => row.status.text)).toEqual([ "Premium aktiv", "Free Account", "Login abgelehnt", "Noch nicht geprüft", "Deaktiviert" ]); expect(rows[0].status.checkedAgo).toBe("vor 5 Min"); expect(JSON.stringify(rows)).not.toContain("test-password"); expect(JSON.stringify(rows)).not.toContain("test-token"); }); it("marks elapsed premium access as expired instead of active", () => { expect(resolveAccountStatusState(false, { valid: true, isPremium: true, premiumUntilMs: NOW - 1 }, NOW)).toBe("free"); expect(resolveAccountStatusState(false, { valid: true, isPremium: true, premiumUntilMs: NOW + 1 }, NOW)).toBe("premium"); expect(resolveAccountStatusState(false, { valid: true, isPremium: true, premiumUntilMs: null }, NOW)).toBe("premium"); const [row] = projectAccountRows([{ ...accountSources()[0], status: { ...accountSources()[0].status, state: "free", premiumUntilMs: NOW - 1, checkedAt: NOW - 60 * 60 * 1000 } }], [], NOW); expect(row.status).toEqual({ tone: "free", text: "Free Account", checkedAgo: "vor 1 Std" }); expect(row.premiumUntilMs).toBeNull(); }); it("sorts positive premium expirations first and prunes vanished selections", () => { const rows = projectAccountRows(accountSources(), [], NOW); const sorted = sortAccountRows(rows); expect(sorted[0].status.tone).toBe("ok"); expect(pruneAccountSelection([rows[0].id, "missing"], rows)).toEqual([rows[0].id]); }); it("filters add options honestly and clears hidden credentials", () => { const options = accountOptions(); expect(filterAccountAddOptions(options, "", "web", []) .map((option) => option.id)).toEqual(["ddownload-login"]); expect(filterAccountAddOptions(options, "api-key", "all", ["realdebrid"]).map((option) => option.id)).toEqual(["debridlink-api"]); expect(filterAccountAddOptions(options, "", "all", ["realdebrid"]).map((option) => option.id)).not.toContain("realdebrid-api"); expect(filterAccountAddOptions(options, "", "all", ["realdebrid"]).map((option) => option.id)).toContain("megadebrid-api"); expect(reconcileAccountAddDraft({ selectedId: "realdebrid-api", login: "member@example.test", password: "test-password", token: "test-token", dailyLimitGb: "10" }, [options[1]])).toEqual({ selectedId: null, login: "", password: "", token: "", dailyLimitGb: "" }); }); it("targets new Mega and Debrid-Link identities without inventing checks for other services", () => { const options = accountOptions(); expect(buildTargetedAccountCheck(options[2], "mda-new")).toEqual({ service: "megadebrid-api", expectedStatusId: "mda-new" }); expect(buildTargetedAccountCheck(options[3], "dlk-new")).toEqual({ service: "debridlink", expectedStatusId: "dlk-new" }); expect(buildTargetedAccountCheck(options[1], "ddownload-new")).toBeNull(); }); it("preserves provider order", () => { expect(buildConfiguredProviderOrder( ["debridlink", "realdebrid", "alldebrid"], ["realdebrid", "alldebrid", "debridlink", "bestdebrid"] )).toEqual(["debridlink", "realdebrid", "alldebrid", "bestdebrid"]); }); it("keeps exact rounded limits and migrates edited identity metadata", () => { const login = "member@example.test"; const oldId = getMegaDebridAccountId(login); const newLogin = "renamed@example.test"; const exactLimit = Math.floor(10.05 * GIB); const settings = { ...defaultSettings(), megaCredentials: `${login}:test-password`, megaLogin: login, megaPassword: "test-password", megaDebridDisabledAccountIds: [oldId], megaDebridAccountDailyLimitBytes: { [oldId]: exactLimit }, megaDebridAccountDailyUsageBytes: { [oldId]: 2 * GIB }, megaDebridAccountTotalUsageBytes: { [oldId]: 20 * GIB } }; const target: AccountEditTarget = { type: "mega", rowKey: "row", kind: "megadebrid-api", service: "megadebrid-api", accountId: oldId }; const renderer = createRendererState(settings); const unchanged = buildAccountReplaceCommand(createAccountEditState(target, renderer.accounts)); const renamed = buildAccountReplaceCommand({ ...createAccountEditState(target, renderer.accounts), login: newLogin }); expect(unchanged.dailyLimitBytes).toBe(exactLimit); expect(unchanged.secret).toBe(""); expect(renamed.identity).toBe(newLogin); expect(renamed.secret).toBe(""); }); }); describe("settings views", () => { it("uses a dedicated high-contrast border for form controls", () => { const theme = readFileSync(new URL("../src/renderer/theme.css", import.meta.url), "utf8"); const css = readFileSync(new URL("../src/renderer/views/settings/settings.css", import.meta.url), "utf8"); expect(theme).toContain("--ui-control-border: #707070;"); expect(theme).toContain("--ui-control-border: #7B8491;"); expect(css.match(/border:\s*1px solid var\(--ui-control-border\);/g)?.length).toBeGreaterThanOrEqual(2); }); it("marks settings sections for one measured vertical selection indicator", () => { const html = renderToStaticMarkup(); expect(html).toContain("ui-sliding-selection ui-sliding-selection-vertical"); expect(html.match(/data-sliding-selection-item="true"/g)).toHaveLength(SETTINGS_SECTIONS.length); expect(html.match(/data-sliding-selection-active="true"/g)).toHaveLength(1); }); it("offers animated language and bounded history retention choices", () => { const form = buildSettingsFormViewModel({ settings: { ...createRendererSettings(defaultSettings()), archivePasswordList: "", notifyUrl: "" }, section: "allgemein", speedLimitInput: "0", scheduleSpeedInputs: {} }); const language = form.groups.flatMap((group) => group.fields).find((field) => field.id === "language"); expect(language).toEqual({ id: "language", kind: "select", label: "Sprache", value: "en", options: [ { value: "en", label: "English" }, { value: "de", label: "Deutsch" } ] }); const historyRetention = form.groups.flatMap((group) => group.fields).find((field) => field.id === "historyRetentionMode"); expect(historyRetention).toEqual({ id: "historyRetentionMode", kind: "select", label: "Verlauf speichern", value: "permanent", options: [ { value: "never", label: "Nie" }, { value: "session", label: "Nur aktuelle Session" }, { value: "permanent-100", label: "Nur letzte 100 Einträge" }, { value: "permanent-250", label: "Nur letzte 250 Einträge" }, { value: "permanent", label: "Dauerhaft" } ] }); const html = renderToStaticMarkup( {}, onChange: () => {} }} model={form} />); expect(html).toContain("class=\"settings-select\""); expect(html).toContain("role=\"combobox\""); expect(html).toContain("role=\"listbox\""); expect(settingsCss).toMatch(/\.settings-select-options\s*\{[^}]*opacity:\s*0[^}]*transform:\s*translateY\(-6px\)[^}]*transition:/s); expect(settingsCss).toMatch(/\.settings-select\.is-open\s+\.settings-select-options\s*\{[^}]*opacity:\s*1[^}]*transform:\s*translateY\(0\)/s); }); it("offers one general switch for interface animations", () => { const form = buildSettingsFormViewModel({ settings: { ...createRendererSettings(defaultSettings()), archivePasswordList: "", notifyUrl: "" }, section: "allgemein", speedLimitInput: "0", scheduleSpeedInputs: {} }); const animation = form.groups.flatMap((group) => group.fields) .find((field) => field.id === "animatePackageDisclosure"); expect(animation).toEqual({ id: "animatePackageDisclosure", kind: "switch", label: "Animationen", value: true }); }); it("projects every notification control in the required order with exact bounds", () => { const form = buildSettingsFormViewModel({ settings: { ...createRendererSettings(defaultSettings()), archivePasswordList: "", notifyUrl: "https://discord.com/api/webhooks/example", notifyOnPackageCompleted: true, notifyOnRemainingBelow: true, notifyOnDownloadStall: true }, section: "allgemein", speedLimitInput: "0", scheduleSpeedInputs: {} }); const fields = form.groups.find((group) => group.id === "general-notifications")?.fields ?? []; expect(fields.map((field) => field.id)).toEqual([ "notifyUrl", "notifyMention", "notifyOnPackageCompleted", "notifyOnPackageFailed", "notifyPackageSuccessMode", "notifyOnRunFinished", "notifyOnRemainingBelow", "notifyRemainingThresholdGb", "notifyOnDownloadStall", "notifyStallAfterSeconds", "notifyStallCooldownMinutes", "notifyOnDownloadRecovery" ]); expect(fields.find((field) => field.id === "notifyPackageSuccessMode")).toEqual({ id: "notifyPackageSuccessMode", kind: "select", label: "Erfolgsmeldungen senden", value: "digest", disabled: false, options: [ { value: "digest", label: "Gesammelt (alle 2 Minuten)" }, { value: "individual", label: "Jedes Paket einzeln" } ] }); expect(fields.find((field) => field.id === "notifyRemainingThresholdGb")).toEqual({ id: "notifyRemainingThresholdGb", kind: "number", label: "Restmengenschwelle (GB)", value: "50", min: 1, max: 100000, disabled: false }); expect(fields.find((field) => field.id === "notifyStallAfterSeconds")).toEqual({ id: "notifyStallAfterSeconds", kind: "number", label: "Stillstand bestätigen nach (Sek.)", value: "90", min: 60, max: 3600, disabled: false }); expect(fields.find((field) => field.id === "notifyStallCooldownMinutes")).toEqual({ id: "notifyStallCooldownMinutes", kind: "number", label: "Frühestens erneut melden nach (Min.)", value: "10", min: 5, max: 1440, disabled: false }); }); it("disables notification controls that depend on an inactive switch", () => { const form = buildSettingsFormViewModel({ settings: { ...createRendererSettings(defaultSettings()), archivePasswordList: "", notifyUrl: "" }, section: "allgemein", speedLimitInput: "0", scheduleSpeedInputs: {} }); const fields = form.groups.find((group) => group.id === "general-notifications")?.fields ?? []; const disabled = Object.fromEntries(fields.map((field) => [field.id, Boolean(field.disabled)])); expect(disabled).toMatchObject({ notifyPackageSuccessMode: true, notifyRemainingThresholdGb: true, notifyStallAfterSeconds: true, notifyStallCooldownMinutes: true, notifyOnDownloadRecovery: true }); }); it("clamps notification number fields and restores their exact defaults", () => { expect(normalizeNotificationNumberField("notifyRemainingThresholdGb", "0")).toBe(1); expect(normalizeNotificationNumberField("notifyRemainingThresholdGb", "100001")).toBe(100000); expect(normalizeNotificationNumberField("notifyRemainingThresholdGb", "invalid")).toBe(50); expect(normalizeNotificationNumberField("notifyStallAfterSeconds", "59")).toBe(60); expect(normalizeNotificationNumberField("notifyStallAfterSeconds", "3601")).toBe(3600); expect(normalizeNotificationNumberField("notifyStallAfterSeconds", "invalid")).toBe(90); expect(normalizeNotificationNumberField("notifyStallCooldownMinutes", "4")).toBe(5); expect(normalizeNotificationNumberField("notifyStallCooldownMinutes", "1441")).toBe(1440); expect(normalizeNotificationNumberField("notifyStallCooldownMinutes", "invalid")).toBe(10); expect(normalizeNotificationNumberField("maxParallel", "8")).toBeUndefined(); }); it("supports keyboard navigation in animated settings selects", () => { expect(getSettingsSelectNavigationIndex(1, 3, "ArrowDown")).toBe(2); expect(getSettingsSelectNavigationIndex(2, 3, "ArrowDown")).toBe(0); expect(getSettingsSelectNavigationIndex(0, 3, "ArrowUp")).toBe(2); expect(getSettingsSelectNavigationIndex(1, 3, "Home")).toBe(0); expect(getSettingsSelectNavigationIndex(1, 3, "End")).toBe(2); const source = readFileSync(new URL("../src/renderer/views/settings/SettingsForm.tsx", import.meta.url), "utf8"); expect(source).toContain("optionRefs.current[nextIndex]?.focus()"); expect(getSettingsSelectKeyboardAction("Home", 1, 3)).toEqual({ type: "focus", index: 0 }); expect(getSettingsSelectKeyboardAction("End", 1, 3)).toEqual({ type: "focus", index: 2 }); expect(source).toContain("onBlur={onBlur}"); }); it("closes settings selects with Escape and restores the trigger focus", () => { const calls: string[] = []; expect(getSettingsSelectKeyboardAction("Escape", 1, 3)).toEqual({ type: "close" }); expect(getSettingsSelectKeyboardAction("ArrowDown", 1, 3)).toEqual({ type: "focus", index: 2 }); expect(getSettingsSelectKeyboardAction("ArrowUp", 0, 3)).toEqual({ type: "focus", index: 2 }); closeSettingsSelectAndRestoreFocus( () => calls.push("close"), { focus: () => calls.push("focus") } ); expect(calls).toEqual(["close", "focus"]); }); it("clears a bounded history preset when permanent retention is selected", () => { expect(resolveHistoryRetentionSelection("permanent", 100, "permanent")).toEqual({ historyRetentionMode: "permanent", historyMaxEntries: 500 }); expect(resolveHistoryRetentionSelection("permanent", 250, "permanent-100")).toEqual({ historyRetentionMode: "permanent", historyMaxEntries: 100 }); }); it("renders one real sidebar marker and all sections", () => { const html = renderToStaticMarkup(); expect(count(html, "data-visual-region=\"settings-sidebar\"")).toBe(1); for (const section of SETTINGS_SECTIONS) { expect(html).toContain(section.label); } expect(html).toContain("aria-current=\"page\""); }); it("shows every save state without a generic toolbar, pagination or info control", () => { for (const saveState of ["clean", "dirty", "saving", "saved", "error"] as const) { const html = renderToStaticMarkup(); expect(html).toContain(getSettingsSaveLabel(saveState)); expect(html).toContain("Einstellungen speichern"); expect(html).not.toContain("role=\"toolbar\""); expect(html).not.toContain("table-pagination"); expect(html).not.toContain("ui-context-info"); } }); it("renders the sidebar and content once in the complete view", () => { const html = renderToStaticMarkup(); expect(count(html, "data-visual-region=\"settings-sidebar\"")).toBe(1); expect(count(html, "data-visual-region=\"accounts-table-body\"")).toBe(1); }); it("renders form controls, theme choices and switches through bounded callbacks", () => { let changed = ""; const form = SettingsForm({ model: formModel(), actions: { onChange: (id) => { changed = id; }, onAction: () => {} } }); const html = renderToStaticMarkup(form); expect(html).toContain("Light"); expect(html).toContain("Dark"); expect(html).toContain("System"); expect(html).toContain("role=\"switch\""); const switchButton = findElement(form, (element) => element.props.role === "switch"); switchButton.props.onClick(); expect(changed).toBe("autoUpdate"); }); it("uses roving focus and complete arrow navigation for theme radios", () => { const changes: string[] = []; const tree = SettingsForm({ model: formModel(), actions: { onChange: (id, value) => changes.push(`${id}:${String(value)}`), onAction: () => {} } }); const radios = findElements(tree, (element) => element.props.role === "radio"); expect(radios.map((radio) => radio.props.tabIndex)).toEqual([-1, 0, -1]); for (const [key, sourceIndex, targetIndex, value] of [ ["ArrowRight", 1, 2, "system"], ["ArrowDown", 1, 2, "system"], ["ArrowLeft", 1, 0, "light"], ["ArrowUp", 1, 0, "light"], ["Home", 1, 0, "light"], ["End", 1, 2, "system"] ] as const) { const target = compositeKeyboardTarget(radios.length); let prevented = false; radios[sourceIndex].props.onKeyDown({ key, currentTarget: target.elements[sourceIndex], preventDefault: () => { prevented = true; } }); expect(prevented).toBe(true); expect(target.focused()).toBe(targetIndex); expect(changes.at(-1)).toBe(`theme:${value}`); } }); }); describe("account workspace", () => { it("marks account panels for one measured horizontal selection indicator", () => { const html = renderToStaticMarkup(); expect(html).toContain("ui-sliding-selection ui-sliding-selection-horizontal"); expect(html.match(/data-sliding-selection-item="true"/g)).toHaveLength(3); expect(html.match(/data-sliding-selection-active="true"/g)).toHaveLength(1); }); it("shows the last account check directly below the status", () => { const html = renderToStaticMarkup(); expect(html).toContain("settings-account-status-checked"); expect(html).toContain("Geprüft"); expect(html).toContain("vor 5 Min"); }); it("shows a live provider and account overview without exposing raw failure details", () => { const html = renderToStaticMarkup( ); expect(html).toContain("Laufzeit"); expect(html).toContain("Real-Debrid"); expect(html).toContain("1 von 2 verfügbar"); expect(html).toContain("3 aktive Downloads"); expect(html).toContain("75 % (3/4)"); expect(html).toContain("Rate-Limit aktiv · 48 Sek."); expect(html).not.toContain("HTTP 429"); expect(html).not.toContain("token-"); }); it("uses roving focus and horizontal keyboard navigation for account tabs", () => { const panels: string[] = []; const tree = AccountWorkspace({ model: workspaceModel(), actions: workspaceActions({ onPanelChange: (panel) => panels.push(panel) }) }); const tabs = findElements(tree, (element) => element.props.role === "tab"); expect(tabs.map((tab) => tab.props.tabIndex)).toEqual([0, -1, -1]); for (const [key, sourceIndex, targetIndex, panel] of [ ["ArrowRight", 0, 1, "rules"], ["ArrowLeft", 0, 2, "runtime"], ["Home", 1, 0, "overview"], ["End", 0, 2, "runtime"] ] as const) { const target = compositeKeyboardTarget(tabs.length); let prevented = false; tabs[sourceIndex].props.onKeyDown({ key, currentTarget: target.elements[sourceIndex], preventDefault: () => { prevented = true; } }); expect(prevented).toBe(true); expect(target.focused()).toBe(targetIndex); expect(panels.at(-1)).toBe(panel); } const rulesTree = AccountWorkspace({ model: { ...workspaceModel(), activePanel: "rules" }, actions: workspaceActions() }); const rulesTabs = findElements(rulesTree, (element) => element.props.role === "tab"); expect(rulesTabs.map((tab) => tab.props.tabIndex)).toEqual([-1, 0, -1]); }); it("announces account loading and errors without hiding the existing table state", () => { const loadingHtml = renderToStaticMarkup( ); const errorHtml = renderToStaticMarkup( ); expect(loadingHtml).toContain('aria-busy="true"'); expect(loadingHtml).toContain('aria-live="polite"'); expect(loadingHtml).toContain('role="status"'); expect(loadingHtml).toContain("Accountdaten werden aktualisiert"); expect(errorHtml).toContain('role="alert"'); expect(errorHtml).toContain("Accounts konnten nicht geladen werden"); }); it("renders the exact columns, one table marker, full usernames and no raw credentials", () => { const html = renderToStaticMarkup(); const positions = ACCOUNT_COLUMNS.map((column) => html.indexOf(column)); expect(positions.every((position) => position >= 0)).toBe(true); expect(positions).toEqual([...positions].sort((a, b) => a - b)); expect(count(html, "data-visual-region=\"accounts-table-body\"")).toBe(1); expect(html).toContain("verified@example.test"); expect(html).not.toContain("ve***st"); expect(html).toContain("••••••"); expect(html).not.toContain("test-password"); expect(html).not.toContain("test-token"); expect(html).not.toContain("table-pagination"); expect(html).not.toContain("role=\"toolbar\""); expect(count(html, 'role="separator"')).toBe(ACCOUNT_COLUMNS.length); expect(html).toContain('aria-label="Status Spaltenbreite ändern"'); expect(html).toMatch(/class="settings-account-column-actions" role="columnheader">Aktion<\/span>/); expect(settingsCss).toMatch(/\.settings-account-column-actions\s*{[^}]*place-items:\s*center end/s); expect(settingsCss).not.toMatch(/\.settings-account-column-actions\s*{[^}]*padding-left:/s); expect(settingsCss).toMatch(/\.settings-account-row > \.settings-account-column-actions\s*{[^}]*padding-right:\s*19px/s); expect(settingsCss).not.toMatch(/\.settings-account-column-actions\s*{[^}]*transform:/s); }); it("offers separate checks for active accounts and every configured account", () => { const html = renderToStaticMarkup(); expect(html).toContain("Aktive aktualisieren"); expect(html).toContain("Alle aktualisieren"); expect(html).toContain('title="Prüft nur aktivierte Accounts."'); expect(html).toContain('title="Prüft alle angelegten Accounts, auch deaktivierte."'); }); it("disables the active account check when no enabled account can be checked", () => { const model = workspaceModel(); const html = renderToStaticMarkup( ({ ...row, enabled: false })) }} /> ); expect(html).toMatch(/]*disabled=""[^>]*>↻ Aktive aktualisieren<\/button>/); expect(html).toMatch(/]*>↻ Alle aktualisieren<\/button>/); }); it("clamps and serializes persistent account table column widths", () => { const api = settingsModel as typeof settingsModel & { createAccountTableColumnWidths?: () => Record; resizeAccountTableColumn?: (widths: Record, column: string, delta: number) => Record; getAccountTableGridTemplate?: (widths: Record) => string; }; expect(api.createAccountTableColumnWidths).toBeTypeOf("function"); expect(api.resizeAccountTableColumn).toBeTypeOf("function"); expect(api.getAccountTableGridTemplate).toBeTypeOf("function"); const initial = api.createAccountTableColumnWidths?.() || {}; const widened = api.resizeAccountTableColumn?.(initial, "status", 120) || {}; const narrowed = api.resizeAccountTableColumn?.(widened, "status", -10_000) || {}; expect(widened.status).toBeGreaterThan(initial.status); expect(narrowed.status).toBeGreaterThanOrEqual(120); expect(api.getAccountTableGridTemplate?.(widened)).toContain(`${widened.status}px`); expect(api.getAccountTableGridTemplate?.(widened)).toMatch(/minmax\(64px, 1fr\)$/); }); it("keeps row selection, enable toggles, edit and context actions separate", () => { const calls: string[] = []; const tree = AccountWorkspace({ model: workspaceModel(), actions: workspaceActions({ onSelect: (id, additive) => calls.push(`select:${id}:${additive}`), onToggleEnabled: (id) => calls.push(`toggle:${id}`), onEdit: (id) => calls.push(`edit:${id}`), onContextMenu: (id) => calls.push(`context:${id}`) }) }); const row = findElement(tree, (element) => element.props.role === "row" && element.props["aria-selected"] === true); const checkbox = findElement(row, (element) => element.type === "input" && element.props.type === "checkbox"); const actionButton = findElement(row, (element) => element.type === "button" && String(element.props["aria-label"] || "").includes("Aktionen")); const rowId = workspaceModel().rows[0].id; row.props.onClick({ target: { role: "cell" }, currentTarget: row, ctrlKey: false, metaKey: false }); row.props.onClick({ target: { role: "cell" }, currentTarget: row, ctrlKey: true, metaKey: false }); row.props.onKeyDown({ key: "Enter", target: row, currentTarget: row, preventDefault: () => {} }); row.props.onKeyDown({ key: " ", target: checkbox, currentTarget: row, preventDefault: () => {} }); checkbox.props.onChange(); row.props.onDoubleClick(); actionButton.props.onClick({ stopPropagation: () => {}, currentTarget: { getBoundingClientRect: () => ({ right: 20, bottom: 30 }) } }); actionButton.props.onDoubleClick({ stopPropagation: () => calls.push("action-double-click-stopped") }); expect(calls).toEqual([ `select:${rowId}:false`, `select:${rowId}:true`, `select:${rowId}:false`, `toggle:${rowId}`, `edit:${rowId}`, `context:${rowId}`, "action-double-click-stopped" ]); }); it("formats the account action heading as hoster and access type without a dangling marker", () => { const api = settingsModel as typeof settingsModel & { formatAccountContextHeading?: (hoster: string, mode: string) => string; }; expect(api.formatAccountContextHeading).toBeTypeOf("function"); expect(api.formatAccountContextHeading?.("Debrid-Link", "API")).toBe("Debrid-Link | API"); expect(api.formatAccountContextHeading?.("Mega-Debrid", "Web-Login")).toBe("Mega-Debrid | Web-Login"); }); it("shows the selected account count", () => { const model = workspaceModel(); const tree = AccountWorkspace({ actions: workspaceActions(), model: { ...model, selectedIds: model.rows.slice(0, 3).map((row) => row.id) } }); const html = renderToStaticMarkup(tree); expect(html).toContain("− Entfernen (3)"); }); it("removes the redundant global account activation switch", () => { const legacyActions = { ...workspaceActions(), onSetAllEnabled: () => {} } as AccountWorkspaceActions; const legacyModel = { ...workspaceModel(), allEnabled: true } as AccountWorkspaceViewModel; const html = renderToStaticMarkup( ); expect(html).not.toContain("Accounts zum Herunterladen verwenden"); expect(settingsCss).toMatch(/\.settings-account-row\.is-selected\s*{[^}]*#5b8cff[^}]*box-shadow:/s); }); it("copies only populated username and email cells without triggering the row", () => { const copies: string[] = []; const tree = AccountWorkspace({ model: workspaceModel(), actions: workspaceActions({ onCopyIdentity: (label, value) => copies.push(`${label}:${value}`) }) }); const buttons = findElements(tree, (element) => String(element.props.className || "").includes("settings-account-copy-button")); const copyableCells = findElements(tree, (element) => ["settings-account-username", "settings-account-email"].includes(String(element.props.className || ""))) .filter((element) => element.props.title === "Klicken zum Kopieren"); const username = buttons.find((button) => button.props["aria-label"] === "Benutzername kopieren"); const email = buttons.find((button) => button.props["aria-label"] === "E-Mail kopieren"); let stopped = 0; username?.props.onClick({ stopPropagation: () => { stopped += 1; } }); email?.props.onClick({ stopPropagation: () => { stopped += 1; } }); username?.props.onDoubleClick({ stopPropagation: () => { stopped += 1; } }); expect(buttons.some((button) => button.props.children === "—")).toBe(false); expect(copyableCells).toHaveLength(buttons.length); expect(copies).toEqual([ "Benutzername:stored-user", "E-Mail:verified@example.test" ]); expect(stopped).toBe(3); }); it("keeps all account panels in the same workspace while only one panel is active", () => { const html = renderToStaticMarkup(); expect(count(html, "class=\"settings-account-panel\"")).toBe(3); expect(count(html, "hidden=\"\"")).toBe(2); expect(html).toContain("Provider-Reihenfolge"); expect(html).toContain("Hoster-Routing"); expect(html).toContain("Automatischer Fallback"); expect(html).toContain("Provider-Laufzeit"); }); it("keeps add and edit dialogs separate and every secret field protected", () => { const options = accountOptions(); const addHtml = renderToStaticMarkup( {}, onServiceFilterChange: () => {}, onOptionSelect: () => {}, onFieldChange: () => {}, onClose: () => {}, onSubmit: () => {} }} model={{ open: true, query: "", serviceFilter: "all", serviceFilters: ["Real-Debrid", "DDownload", "Mega-Debrid", "Debrid-Link"], options, selectedOptionId: "megadebrid-api", fields: [ { id: "login", label: "Login", type: "text", value: "member@example.test" }, { id: "password", label: "Passwort", type: "password", value: "test-password" } ], error: "", busy: false }} /> ); const editHtml = renderToStaticMarkup( {}, onClose: () => {}, onCheck: () => {}, onSave: () => {}, onRemove: () => {}, onToggleEnabled: () => {}, onToggleSecret: () => {}, onCopySecret: () => {} }} model={{ open: true, hoster: "Mega-Debrid", mode: "API", identity: "member@example.test", enabled: true, fields: [ { id: "login", label: "Login", type: "text", value: "member@example.test" }, { id: "password", label: "Passwort", type: "password", value: "", storedSecret: true, secretVisible: false }, { id: "token", label: "Token", type: "password", value: "", storedSecret: true, secretVisible: false }, { id: "dailyLimitGb", label: "Tageslimit (GB, optional)", type: "number", value: "" } ], error: "", busy: false }} /> ); expect(addHtml).toContain("Account hinzufügen"); expect(addHtml).toContain("Prüfen und speichern"); expect(count(addHtml, " expect(addHtml).toContain(`data-account-option-id="${option.id}"`)); expect(addHtml).toContain('data-account-option-id="megadebrid-api"'); expect(addHtml).toContain('aria-selected="true"'); expect(addHtml).toContain("Weiteren Account hinzufügen"); expect(addHtml).toContain("Login:Passwort"); expect(addHtml).toContain('type="search"'); expect(addHtml).toContain("settings-account-picker-row"); expect(count(addHtml, 'class="settings-account-dialog-fields"')).toBe(1); expect(addHtml.indexOf('aria-label="Dienst oder Zugangstyp suchen"')).toBeLessThan(addHtml.indexOf("settings-account-picker-table")); expect(addHtml.indexOf("settings-account-picker-table")).toBeLessThan(addHtml.indexOf("settings-account-dialog-fields")); expect(editHtml).toContain("Account bearbeiten"); expect(editHtml).toContain("member@example.test"); expect(editHtml).toContain("Entfernen"); expect(editHtml).toContain("Prüfen"); expect(count(addHtml, "type=\"password\"")).toBe(1); expect(count(editHtml, "type=\"password\"")).toBe(2); expect(editHtml).toContain('aria-label="Passwort anzeigen"'); expect(editHtml).toContain('aria-label="Token anzeigen"'); expect(editHtml.indexOf("Tageslimit (GB, optional)")).toBeLessThan(editHtml.indexOf("Account aktiviert")); }); it("selects the account option through the compact service table", () => { const selected: string[] = []; const tree = AccountAddDialog({ actions: { onQueryChange: () => {}, onServiceFilterChange: () => {}, onOptionSelect: (optionId) => selected.push(optionId), onFieldChange: () => {}, onClose: () => {}, onSubmit: () => {} }, model: { open: true, query: "", serviceFilter: "all", serviceFilters: ["Real-Debrid", "DDownload", "Mega-Debrid", "Debrid-Link"], options: accountOptions(), selectedOptionId: "megadebrid-api", fields: [], error: "", busy: false } }); const selector = findElement(tree, (element) => element.props["data-account-option-id"] === "debridlink-api"); selector.props.onClick(); expect(selected).toEqual(["debridlink-api"]); }); it("filters by service and selects the first visible option with Enter", () => { const selected: string[] = []; const serviceFilters: string[] = []; const options = accountOptions().filter((option) => option.title === "Mega-Debrid"); const tree = AccountAddDialog({ actions: { onQueryChange: () => {}, onServiceFilterChange: (service) => serviceFilters.push(service), onOptionSelect: (optionId) => selected.push(optionId), onFieldChange: () => {}, onClose: () => {}, onSubmit: () => {} }, model: { open: true, query: "api", serviceFilter: "Mega-Debrid", serviceFilters: ["Real-Debrid", "Mega-Debrid"], options, selectedOptionId: options[0].id, fields: [], error: "", busy: false } }); const serviceSelect = findElement(tree, (element) => element.props["aria-label"] === "Dienst filtern"); const search = findElement(tree, (element) => element.props["aria-label"] === "Dienst oder Zugangstyp suchen"); serviceSelect.props.onChange({ target: { value: "Real-Debrid" } }); search.props.onKeyDown({ key: "Enter", preventDefault: () => selected.push("prevented") }); expect(serviceFilters).toEqual(["Real-Debrid"]); expect(selected).toEqual(["prevented", "megadebrid-api"]); }); it("shows an accessible empty result when account search has no matches", () => { const html = renderToStaticMarkup( {}, onServiceFilterChange: () => {}, onOptionSelect: () => {}, onFieldChange: () => {}, onClose: () => {}, onSubmit: () => {} }} model={{ open: true, query: "nicht vorhanden", serviceFilter: "all", serviceFilters: ["Real-Debrid"], options: [], selectedOptionId: null, fields: [], error: "", busy: false }} /> ); expect(html).toContain('aria-describedby="settings-account-picker-empty"'); expect(html).toContain('aria-live="polite"'); expect(html).toContain('role="status"'); expect(html).toContain("Keine passenden Dienste oder Zugangstypen gefunden"); }); it("keeps stored usernames separate from provider email addresses", () => { const rows = projectAccountRows(accountSources(), [], NOW); expect(rows[0].username).toBe("stored-user"); expect(rows[0].email).toBe("verified@example.test"); expect(ACCOUNT_COLUMNS).toContain("E-Mail"); const html = renderToStaticMarkup(); expect(html).toContain("stored-user"); expect(html).toContain("verified@example.test"); }); }); describe("settings App integration", () => { it("preserves the write-only webhook during live snapshots and clears it for backup reseeding", () => { const safe = createRendererSettings({ ...defaultSettings(), notifyUrl: "https://private.example.test/hook" }); const current = { ...safe, archivePasswordList: "loaded-password", notifyUrl: "https://private.example.test/hook" }; expect(createSettingsDraft(safe, current).notifyUrl).toBe("https://private.example.test/hook"); expect(createSettingsDraft(safe).notifyUrl).toBe(""); expect(appSource).toContain("applyPersistedSettings(fresh.settings, false)"); }); it("loads and preserves the stored archive password list in the extraction section", () => { const revealBlock = sourceBlock(appSource, "const showToast", "const clearImportQueueFocusListener"); const applyBlock = sourceBlock(appSource, "const applyPersistedSettings", "const syncLiveProviderUsageSettings"); const mainSource = readFileSync(new URL("../src/main/main.ts", import.meta.url), "utf8"); expect(revealBlock).toContain("window.rd.getArchivePasswordList()"); expect(revealBlock).toContain('settingsSubTab !== "extract"'); expect(appSource).toContain("setSettingsDraft((current) => createSettingsDraft(state.settings, current))"); expect(appSource).toContain("setSettingsDraft((current) => createSettingsDraft(next.settings, current))"); expect(applyBlock).toContain("setSettingsDraft((current) => createSettingsDraft(result, preserveWriteOnlyValues ? current : undefined))"); expect(mainSource).toContain("handleTrusted(IPC_CHANNELS.GET_ARCHIVE_PASSWORD_LIST"); }); it("persists an edited archive password list before unrelated account mutations", () => { const editBlock = sourceBlock(appSource, "const onSaveAccountEditDialog", "const onSaveAccountDialog"); const createBlock = sourceBlock(appSource, "const onSaveAccountDialog", "const onResetAccountDailyUsage"); const deleteKeyBlock = sourceBlock(appSource, "const onRemoveDebridLinkKey", "const onToggleAccountEnabled"); const deleteRowsBlock = sourceBlock(appSource, "const removeAccountTableRows", "const checkAccountsActive"); expect(editBlock.indexOf("await persistDraftSettings()")).toBeLessThan(editBlock.indexOf("window.rd.replaceAccount")); expect(createBlock.indexOf("await persistDraftSettings()")).toBeLessThan(createBlock.indexOf("window.rd.createAccount")); expect(deleteKeyBlock.indexOf("await persistDraftSettings()")).toBeLessThan(deleteKeyBlock.indexOf("window.rd.deleteAccount")); expect(deleteRowsBlock.indexOf("await persistDraftSettings()")).toBeLessThan(deleteRowsBlock.indexOf("window.rd.deleteAccount")); }); it("invalidates an archive password reveal before applying imported settings", () => { const applyBlock = sourceBlock(appSource, "const applyPersistedSettings", "const syncLiveProviderUsageSettings"); expect(applyBlock).toContain("archivePasswordLoadGenerationRef.current += 1"); expect(applyBlock.indexOf("archivePasswordLoadGenerationRef.current += 1")).toBeLessThan(applyBlock.indexOf("setSettingsDraft")); }); it("enables an account by clearing both its row-level and provider-level locks", () => { expect(buildScopedAccountEnabledState( ["debridlink", "linksnappy"], ["debridlink"], ["key-disabled", "key-other"], "key-disabled", true )).toEqual({ disabledProviders: ["linksnappy"], disabledAccountIds: ["key-other"] }); expect(buildScopedAccountEnabledState([], ["debridlink"], [], "key-active", false)).toEqual({ disabledProviders: [], disabledAccountIds: ["key-active"] }); const debridLinkBlock = sourceBlock(appSource, "const onToggleDebridLinkApiKeyEnabled", "const onAccountRowQuickAction"); const megaBlock = sourceBlock(appSource, "const onToggleMegaAccountEnabled", "const onRemoveDebridLinkKey"); expect(debridLinkBlock).toContain("buildScopedAccountEnabledState"); expect(megaBlock).toContain("buildScopedAccountEnabledState"); expect(megaBlock).toContain('megaDebridApiEnabled: mode === "api" && enabled ? true'); expect(megaBlock).toContain('megaDebridWebEnabled: mode === "web" && enabled ? true'); }); it("shows a failed all-account check even when the account is disabled", () => { expect(resolveAccountStatusState(true, { valid: false, isPremium: false })).toBe("invalid"); expect(resolveAccountStatusState(true, { valid: true, isPremium: true })).toBe("disabled"); expect(resolveAccountStatusState(false, undefined)).toBe("unchecked"); }); it("applies account switches before persistence settles and rolls back failed saves", async () => { const events: string[] = []; let resolvePersist: (value: string) => void = () => { throw new Error("persist resolver missing"); }; const pending = runOptimisticAccountUpdate( () => events.push("apply"), () => new Promise((resolve) => { resolvePersist = resolve; }), () => events.push("rollback") ); expect(events).toEqual(["apply"]); resolvePersist("saved"); await expect(pending).resolves.toBe("saved"); expect(events).toEqual(["apply"]); await expect(runOptimisticAccountUpdate( () => events.push("apply-failed"), async () => { throw new Error("save failed"); }, () => events.push("rollback") )).rejects.toThrow("save failed"); expect(events.slice(-2)).toEqual(["apply-failed", "rollback"]); }); it("keeps new Mega-Debrid credentials empty and never exposes stored accounts as an API key", () => { const settings = { ...defaultSettings(), megaCredentials: "first@example.test:first-secret\nsecond@example.test:second-secret", debridLinkApiKeys: "existing-debrid-link-key" }; const rendererSettings = createRendererSettings(settings); const megaDialog = createAccountDialogState("create", "megadebrid-api", rendererSettings); const megaFields = buildAccountAddFields(megaDialog); const debridLinkFields = buildAccountAddFields(createAccountDialogState("create", "debridlink-api", rendererSettings)); expect(megaDialog.megaNewLogin).toBe(""); expect(megaDialog.megaNewPassword).toBe(""); expect(megaDialog.token).toBe(""); expect(megaFields.map((field) => field.id)).toEqual(["megaNewLogin", "megaNewPassword", "dailyLimitGb"]); expect(megaFields.map((field) => field.label)).not.toContain("Token / API-Key"); expect(debridLinkFields.find((field) => field.id === "token")).toEqual(expect.objectContaining({ value: "" })); }); it("keeps specific persistence revision-safe when the draft changes in flight", () => { const block = sourceBlock(appSource, "const persistSpecificSettings", "const runAccountQuickAction"); expect(block).toContain("revisionAtStart"); expect(block).toContain("mergeConcurrentSpecificSettings"); expect(block).toContain('setSettingsSaveState("dirty")'); }); it("keeps unchecked single accounts honest without a positive status", () => { const block = sourceBlock(appSource, "const accountSources", "const selectedAccountViewId"); expect(block).toContain("resolveAccountStatusState(row.disabled, checkedStatus, runtimeNow)"); }); it("stores only the stable account row id in context-menu state", () => { const stateBlock = sourceBlock(appSource, "interface AccountContextMenuState", "function getAccountQuickActionMeta"); expect(stateBlock).toContain("rowId: string"); expect(stateBlock).not.toContain("row: AccountTableRow"); expect(appSource).toContain("activeAccountContextRow"); }); it("preserves the System theme choice while applying its resolved palette", () => { expect(appSource).toContain("settingsThemeChoice"); expect(appSource).toContain("resolveSettingsThemeChoice"); }); }); describe("settings geometry", () => { it("uses central focus and semantic status text tokens", () => { expect(settingsCss).toContain(".settings-account-status-checked"); expect(settingsCss).toMatch(/\.settings-theme-option:focus-visible,[^{]*\.settings-account-picker-row:focus-visible\s*{[^}]*outline:\s*2px solid var\(--ui-focus\);/s); expect(settingsCss).toMatch(/\.settings-save-state\.is-clean,[^{]*\.settings-save-state\.is-saved\s*{[^}]*color:\s*var\(--ui-success-text\);/s); expect(settingsCss).toMatch(/\.settings-save-state\.is-dirty,[^{]*\.settings-save-state\.is-saving\s*{[^}]*color:\s*var\(--ui-warning-text\);/s); expect(settingsCss).toMatch(/\.settings-save-state\.is-error\s*{[^}]*color:\s*var\(--ui-danger-text\);/s); expect(settingsCss).toMatch(/\.settings-account-status-badge\.is-ok\s*{[^}]*color:\s*var\(--ui-success-text\);/s); expect(settingsCss).toMatch(/\.settings-account-status-badge\.is-free,[^{]*\.settings-account-status-badge\.is-unknown\s*{[^}]*color:\s*var\(--ui-warning-text\);/s); expect(settingsCss).toMatch(/\.settings-account-status-badge\.is-invalid\s*{[^}]*color:\s*var\(--ui-danger-text\);/s); expect(settingsCss).toMatch(/\.settings-account-table-error \.ui-data-table-empty-title,[^{]*\.settings-account-dialog-error\s*{[^}]*color:\s*var\(--ui-danger-text\);/s); }); it("keeps the specified form, table, switch, overflow and selection geometry", () => { const css = readFileSync(new URL("../src/renderer/views/settings/settings.css", import.meta.url), "utf8"); expect(css).toMatch(/\.settings-account-picker-list\s*{[^}]*height:\s*190px;[^}]*scrollbar-gutter:\s*stable;/s); expect(css).toMatch(/\.settings-account-picker-empty\s*{[^}]*height:\s*190px;/s); expect(css).toMatch(/\.settings-content\s*{[^}]*padding:\s*24px;/s); expect(css).toMatch( /\.md-runtime-view-content\s*>\s*\.settings-content\s*{[^}]*height:\s*100%;[^}]*padding:\s*24px;/s ); expect(css).toMatch(/\.settings-form-column\s*{[^}]*width:\s*500px;[^}]*max-width:\s*100%;/s); expect(css).toMatch(/\.settings-control\s*{[^}]*height:\s*44px;[^}]*border-radius:\s*6px;/s); expect(css).toMatch(/\.settings-switch\s*{[^}]*width:\s*40px;[^}]*height:\s*20px;/s); expect(css).toMatch(/\.settings-switch\.is-on\s*{[^}]*border-color:\s*var\(--ui-success\);[^}]*background:\s*var\(--ui-success\);/s); expect(css).toMatch(/\.settings-account-table-header\s*{[^}]*height:\s*41px;/s); expect(css).toMatch(/\.settings-account-table-header\s*{[^}]*overflow:\s*hidden;/s); expect(css).toMatch(/\.settings-account-table-grid\s*{[^}]*color:\s*var\(--ui-text\);/s); expect(css).toMatch(/\.settings-account-row\s*{[^}]*height:\s*48px;/s); expect(css).toMatch(/\.settings-account-table-body\s*{[^}]*overflow:\s*auto;/s); expect(css).toMatch(/\.settings-account-status-badge\.is-ok::before\s*{[^}]*background:\s*var\(--ui-success\);/s); expect(accountWorkspaceSource).toContain("onScroll={syncAccountTableScroll}"); expect(css).toMatch(/\.settings-view\s*{[^}]*min-width:\s*0;/s); expect(css).toMatch(/\.settings-account-workspace\s*{[^}]*width:\s*100%;[^}]*min-width:\s*0;[^}]*min-height:\s*0;/s); expect(css).toMatch(/\.settings-static\s*{[^}]*user-select:\s*none;/s); expect(css).toMatch(/\.settings-content\s+:where\(input,\s*textarea,\s*\[contenteditable="true"\],\s*\.settings-copyable\)\s*{[^}]*user-select:\s*text;/s); }); });