From bd2808a763d76fbdf77c627088bd4428a685c319 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Tue, 25 Aug 2026 04:53:53 +0200 Subject: [PATCH] feat(settings): discard unsaved changes --- src/renderer/App.tsx | 157 ++++++++++++++++--- src/renderer/i18n.ts | 2 +- src/renderer/views/settings/SettingsView.tsx | 40 +++-- src/renderer/views/settings/settings.css | 7 + tests/i18n.test.ts | 4 + tests/settings-view.test.tsx | 77 ++++++++- 6 files changed, 244 insertions(+), 43 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 1396683..8e3b385 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -339,6 +339,33 @@ export function createSettingsDraft(settings: RendererSettings, current?: Render }; } +export function createDiscardedSettingsState(settings: RendererSettings, themeChoice: SettingsThemeChoice = settings.theme): { + draft: RendererSettingsDraft; + themeChoice: SettingsThemeChoice; + speedLimitInput: string; + scheduleSpeedInputs: Record; +} { + return { + draft: createSettingsDraft(settings), + themeChoice, + speedLimitInput: formatMbpsInputFromKbps(settings.speedLimitKbps), + scheduleSpeedInputs: Object.fromEntries( + (settings.bandwidthSchedules || []).map((schedule) => [schedule.id, formatMbpsInputFromKbps(schedule.speedLimitKbps)]) + ) + }; +} + +export function resolveSettingsSaveCompletion(revisionAtStart: number, currentRevision: number): { + saveState: Extract; + applyPersistedTheme: boolean; + toast: string; +} { + const unchanged = revisionAtStart === currentRevision; + return unchanged + ? { saveState: "saved", applyPersistedTheme: true, toast: "Einstellungen gespeichert" } + : { saveState: "dirty", applyPersistedTheme: false, toast: "Zwischenstand gespeichert – weitere Änderungen sind ungespeichert" }; +} + function settingsValueEqual(left: unknown, right: unknown): boolean { return Object.is(left, right) || JSON.stringify(left) === JSON.stringify(right); } @@ -1670,6 +1697,7 @@ export function App(): ReactElement { const [scheduleSpeedInputs, setScheduleSpeedInputs] = useState>({}); const [settingsDirty, setSettingsDirty] = useState(false); const [settingsSaveState, setSettingsSaveState] = useState("clean"); + const [settingsSaveInFlight, setSettingsSaveInFlight] = useState(false); const [schedulePickerOpen, setSchedulePickerOpen] = useState(false); const [scheduleTimeInput, setScheduleTimeInput] = useState(""); const [scheduleStartDay, setScheduleStartDay] = useState("today"); @@ -1678,6 +1706,7 @@ export function App(): ReactElement { const updateCheckGenerationRef = useRef(0); const dismissedUpdateTagRef = useRef(""); const settingsDirtyRef = useRef(false); + const settingsSaveInFlightRef = useRef(false); const writeOnlySettingsDirtyRef = useRef(new Set<"archivePasswordList" | "notifyUrl">()); const archivePasswordLoadGenerationRef = useRef(0); const settingsDraftRevisionRef = useRef(0); @@ -1687,10 +1716,14 @@ export function App(): ReactElement { return () => localizer.disconnect(); }, [settingsDraft.language]); const panelDirtyRevisionRef = useRef(0); - const latestStateRef = useRef(null); - const masterSnapshotRef = useRef(null); - const snapshotRef = useRef(snapshot); - snapshotRef.current = snapshot; + const latestStateRef = useRef(null); + const masterSnapshotRef = useRef(null); + const snapshotRef = useRef(snapshot); + const persistedSettingsRef = useRef(emptySnapshot().settings); + const settingsThemeChoiceRef = useRef(settingsThemeChoice); + const persistedThemeChoiceRef = useRef(emptySnapshot().settings.theme); + snapshotRef.current = snapshot; + settingsThemeChoiceRef.current = settingsThemeChoice; const tabRef = useRef(tab); tabRef.current = tab; const stateFlushTimerRef = useRef | null>(null); @@ -2129,6 +2162,8 @@ export function App(): ReactElement { return; } masterSnapshotRef.current = state; + persistedSettingsRef.current = state.settings; + persistedThemeChoiceRef.current = state.settings.theme; setSnapshot(state); if (state.settings.columnOrder?.length > 0) { columnOrderPersistenceRef.current?.applyAuthoritative(state.settings.columnOrder); @@ -2140,6 +2175,7 @@ export function App(): ReactElement { setSettingsDirty(false); setSettingsSaveState("clean"); setSettingsThemeChoice(state.settings.theme); + settingsThemeChoiceRef.current = state.settings.theme; applyTheme(state.settings.theme); if (state.settings.autoUpdateCheck) { void runLatestUpdateCheck( @@ -2174,7 +2210,8 @@ export function App(): ReactElement { } else { merged = wireState; } - masterSnapshotRef.current = merged; + masterSnapshotRef.current = merged; + persistedSettingsRef.current = merged.settings; latestStateRef.current = merged; if (stateFlushTimerRef.current) { return; } @@ -2809,16 +2846,57 @@ export function App(): ReactElement { return; } const revisionAtStart = settingsDraftRevisionRef.current; + const themeChoiceAtStart = settingsThemeChoiceRef.current; + settingsSaveInFlightRef.current = true; + setSettingsSaveInFlight(true); setSettingsSaveState("saving"); - await performQuickAction(async () => { - const result = await persistDraftSettings(); - applyTheme(result.theme); - setSettingsSaveState(settingsDraftRevisionRef.current === revisionAtStart ? "saved" : "dirty"); - showToast("Einstellungen gespeichert", 1800); - }, (error) => { - setSettingsSaveState("error"); - showToast(`Einstellungen konnten nicht gespeichert werden: ${String(error)}`, 2800); - }); + try { + await performQuickAction(async () => { + const result = await persistDraftSettings(themeChoiceAtStart); + const completion = resolveSettingsSaveCompletion(revisionAtStart, settingsDraftRevisionRef.current); + if (completion.applyPersistedTheme) { + applyTheme(result.theme); + } + setSettingsSaveState(completion.saveState); + showToast(completion.toast, 1800); + }, (error) => { + setSettingsSaveState("error"); + showToast(`Einstellungen konnten nicht gespeichert werden: ${String(error)}`, 2800); + }); + } finally { + settingsSaveInFlightRef.current = false; + setSettingsSaveInFlight(false); + } + }; + + const discardSettingsChanges = (): void => { + if (settingsSaveInFlightRef.current || actionBusyRef.current || settingsSaveState === "saving" || !settingsDirtyRef.current) { + return; + } + const restored = createDiscardedSettingsState(persistedSettingsRef.current, persistedThemeChoiceRef.current); + settingsDraftRevisionRef.current += 1; + archivePasswordLoadGenerationRef.current += 1; + panelDirtyRevisionRef.current = 0; + writeOnlySettingsDirtyRef.current.clear(); + settingsDirtyRef.current = false; + setSettingsDirty(false); + setSettingsSaveState("clean"); + setSettingsDraft(restored.draft); + setSettingsThemeChoice(restored.themeChoice); + settingsThemeChoiceRef.current = restored.themeChoice; + setSpeedLimitInput(restored.speedLimitInput); + setScheduleSpeedInputs(restored.scheduleSpeedInputs); + applyTheme(restored.draft.theme); + showToast("Ungespeicherte Änderungen verworfen", 1800); + if (settingsSubTab === "extract") { + const generation = archivePasswordLoadGenerationRef.current; + void window.rd.getArchivePasswordList().then(({ passwords }) => { + if (archivePasswordLoadGenerationRef.current !== generation || writeOnlySettingsDirtyRef.current.has("archivePasswordList")) { + return; + } + setSettingsDraft((current) => ({ ...current, archivePasswordList: passwords })); + }).catch(() => undefined); + } }; const onOpenRealDebridLogin = async (): Promise => { @@ -2855,7 +2933,13 @@ export function App(): ReactElement { }); }; - const applyPersistedSettings = (result: RendererSettings, preserveWriteOnlyValues = true): void => { + const applyPersistedSettings = ( + result: RendererSettings, + preserveWriteOnlyValues = true, + themeChoice: SettingsThemeChoice = settingsThemeChoiceRef.current === "system" ? "system" : result.theme + ): void => { + persistedSettingsRef.current = result; + persistedThemeChoiceRef.current = themeChoice; if (!preserveWriteOnlyValues) { archivePasswordLoadGenerationRef.current += 1; } @@ -2868,12 +2952,14 @@ export function App(): ReactElement { panelDirtyRevisionRef.current = 0; setSettingsDirty(false); setSettingsSaveState("clean"); - setSettingsThemeChoice((current) => current === "system" ? current : result.theme); + setSettingsThemeChoice(themeChoice); + settingsThemeChoiceRef.current = themeChoice; applyTheme(result.theme); }; const syncLiveProviderUsageSettings = (result: RendererSettings): void => { - setSnapshot((prev) => ({ ...prev, settings: result })); + persistedSettingsRef.current = result; + setSnapshot((prev) => ({ ...prev, settings: result })); if (!settingsDirtyRef.current) { applyPersistedSettings(result); return; @@ -2891,9 +2977,15 @@ export function App(): ReactElement { })); }; - const persistSpecificSettings = async (nextDraft: RendererSettingsDraft): Promise => { - const revisionAtStart = settingsDraftRevisionRef.current; - const draftAtStart = settingsDraft; + const persistSpecificSettings = async ( + nextDraft: RendererSettingsDraft, + persistenceContext = { + revisionAtStart: settingsDraftRevisionRef.current, + draftAtStart: settingsDraft, + themeChoiceAtStart: settingsThemeChoiceRef.current + } + ): Promise => { + const { revisionAtStart, draftAtStart, themeChoiceAtStart } = persistenceContext; const normalizedDraft = { ...nextDraft, ...normalizeProviderSelectionForSettings(nextDraft) @@ -2902,8 +2994,10 @@ export function App(): ReactElement { if (!writeOnlySettingsDirtyRef.current.has("archivePasswordList")) delete update.archivePasswordList; if (!writeOnlySettingsDirtyRef.current.has("notifyUrl")) delete update.notifyUrl; const result = await window.rd.updateSettings(update); + persistedSettingsRef.current = result; + persistedThemeChoiceRef.current = themeChoiceAtStart; if (settingsDraftRevisionRef.current === revisionAtStart) { - applyPersistedSettings(result); + applyPersistedSettings(result, true, themeChoiceAtStart); } else { setSettingsDraft((current) => mergeConcurrentSpecificSettings(draftAtStart, normalizedDraft, result, current)); settingsDirtyRef.current = true; @@ -3103,7 +3197,13 @@ export function App(): ReactElement { const previousDraft = settingsDraft; const previousDirty = settingsDirtyRef.current; const previousSaveState = settingsSaveState; + const themeChoiceAtStart = settingsThemeChoiceRef.current; const revision = ++settingsDraftRevisionRef.current; + const persistenceContext = { + revisionAtStart: revision, + draftAtStart: previousDraft, + themeChoiceAtStart + }; return runOptimisticAccountUpdate( () => { settingsDirtyRef.current = true; @@ -3113,7 +3213,7 @@ export function App(): ReactElement { }, () => runAccountEnableRefresh( refreshBeforePersist, - () => persistSpecificSettings(nextDraft) + () => persistSpecificSettings(nextDraft, persistenceContext) ), () => { if (settingsDraftRevisionRef.current !== revision) return; @@ -3351,15 +3451,17 @@ export function App(): ReactElement { }); }; - const persistDraftSettings = async (): Promise => { + const persistDraftSettings = async (themeChoiceAtStart: SettingsThemeChoice = settingsThemeChoiceRef.current): Promise => { const revisionAtStart = settingsDraftRevisionRef.current; const update: RendererSettingsUpdate = { ...normalizedSettingsDraft }; if (!writeOnlySettingsDirtyRef.current.has("archivePasswordList")) delete update.archivePasswordList; if (!writeOnlySettingsDirtyRef.current.has("notifyUrl")) delete update.notifyUrl; const result = await window.rd.updateSettings(update); - if (settingsDraftRevisionRef.current === revisionAtStart) { - applyPersistedSettings(result); - } + persistedSettingsRef.current = result; + persistedThemeChoiceRef.current = themeChoiceAtStart; + if (settingsDraftRevisionRef.current === revisionAtStart) { + applyPersistedSettings(result, true, themeChoiceAtStart); + } return result; }; @@ -4959,6 +5061,7 @@ export function App(): ReactElement { const applyAuthoritativeDailyScheduleSnapshot = useCallback((state: UiSnapshot): void => { masterSnapshotRef.current = state; + persistedSettingsRef.current = state.settings; latestStateRef.current = null; snapshotRef.current = state; setSnapshot(state); @@ -5717,11 +5820,13 @@ export function App(): ReactElement { const settingsViewModel: SettingsViewModel = { section: settingsSubTab, saveState: settingsDirty && settingsSaveState === "clean" ? "dirty" : settingsSaveState, + saveInFlight: settingsSaveInFlight, form: settingsFormModel, accounts: accountWorkspaceModel }; const settingsViewActions: SettingsViewActions = { onSectionChange: setSettingsSubTab, + onDiscard: discardSettingsChanges, onSave: () => { void onSaveSettings(); }, form: settingsFormActions, accounts: accountWorkspaceActions diff --git a/src/renderer/i18n.ts b/src/renderer/i18n.ts index 5e3a4bb..ad8a6b0 100644 --- a/src/renderer/i18n.ts +++ b/src/renderer/i18n.ts @@ -6,7 +6,7 @@ const pairs = [ ["Hauptnavigation", "Main navigation"], ["Globale Aktionen", "Global actions"], ["Anwendungsmenü", "Application menu"], ["Seitenleiste einklappen", "Collapse sidebar"], ["Seitenleiste ausklappen", "Expand sidebar"], ["Aktuelle Download-Geschwindigkeit (geglättet)", "Current download speed (smoothed)"], ["Einstellungsbereich", "Settings area"], ["Entpacken", "Extraction"], ["Geschwindigkeit", "Speed"], ["Bereinigung", "Cleanup"], ["Updates", "Updates"], - ["Einstellungen speichern", "Save settings"], ["Gespeichert", "Saved"], ["Ungespeicherte Änderungen", "Unsaved changes"], ["Wird gespeichert…", "Saving…"], ["Speichern fehlgeschlagen", "Save failed"], + ["Einstellungen speichern", "Save settings"], ["Änderungen verwerfen", "Discard changes"], ["Stellt den letzten gespeicherten Stand wieder her.", "Restores the last saved settings."], ["Ungespeicherte Änderungen verworfen", "Unsaved changes discarded"], ["Zwischenstand gespeichert – weitere Änderungen sind ungespeichert", "Progress saved – additional changes remain unsaved"], ["Gespeichert", "Saved"], ["Ungespeicherte Änderungen", "Unsaved changes"], ["Wird gespeichert…", "Saving…"], ["Speichern fehlgeschlagen", "Save failed"], ["Sprache", "Language"], ["Speicherort", "Storage location"], ["Download-Verhalten", "Download behavior"], ["Oberfläche und Bedienung", "Interface and controls"], ["Discord-Benachrichtigungen", "Discord notifications"], ["Speicherort, Download-Verhalten, Verlauf, Oberfläche und Benachrichtigungen.", "Storage location, download behavior, history, interface and notifications."], ["Download-Ordner", "Download folder"], ["Paketname (optional)", "Package name (optional)"], ["Max. gleichzeitige Downloads", "Max. concurrent downloads"], ["Automatische Wiederholungen", "Automatic retries"], diff --git a/src/renderer/views/settings/SettingsView.tsx b/src/renderer/views/settings/SettingsView.tsx index a7fb8a2..58ca991 100644 --- a/src/renderer/views/settings/SettingsView.tsx +++ b/src/renderer/views/settings/SettingsView.tsx @@ -17,16 +17,18 @@ import "./settings.css"; export type SettingsViewRegion = "all" | "sidebar" | "content"; -export interface SettingsViewModel { - section: SettingsSection; - saveState: SettingsSaveState; +export interface SettingsViewModel { + section: SettingsSection; + saveState: SettingsSaveState; + saveInFlight: boolean; form: SettingsFormViewModel; accounts: AccountWorkspaceViewModel; } -export interface SettingsViewActions { - onSectionChange: (section: SettingsSection) => void; - onSave: () => void; +export interface SettingsViewActions { + onSectionChange: (section: SettingsSection) => void; + onDiscard: () => void; + onSave: () => void; form: SettingsFormActions; accounts: AccountWorkspaceActions; } @@ -61,8 +63,9 @@ export function SettingsSidebar({ model, actions }: SettingsViewProps): ReactEle } export function SettingsContent({ model, actions }: SettingsViewProps): ReactElement { - const saveLabel = getSettingsSaveLabel(model.saveState); - const saveDisabled = model.saveState === "clean" || model.saveState === "saved" || model.saveState === "saving"; + const saveLabel = getSettingsSaveLabel(model.saveState); + const saveDisabled = model.saveInFlight || model.saveState === "clean" || model.saveState === "saved" || model.saveState === "saving"; + const discardDisabled = model.saveInFlight || model.saveState === "clean" || model.saveState === "saved" || model.saveState === "saving"; return (
@@ -70,12 +73,21 @@ export function SettingsContent({ model, actions }: SettingsViewProps): ReactEle

Einstellungen

{saveLabel} - +
+ + +
{model.section === "accounts" diff --git a/src/renderer/views/settings/settings.css b/src/renderer/views/settings/settings.css index 6c6f050..eda0b1c 100644 --- a/src/renderer/views/settings/settings.css +++ b/src/renderer/views/settings/settings.css @@ -98,6 +98,13 @@ gap: 12px; } +.settings-content-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + .settings-content-header h1, .settings-form-heading h2, .settings-account-heading h2, diff --git a/tests/i18n.test.ts b/tests/i18n.test.ts index 866683a..ce09e32 100644 --- a/tests/i18n.test.ts +++ b/tests/i18n.test.ts @@ -11,6 +11,10 @@ describe("renderer localization", () => { it("translates exact interface labels in both directions", () => { expect(translateUiText("Einstellungen speichern", "en")).toBe("Save settings"); expect(translateUiText("Save settings", "de")).toBe("Einstellungen speichern"); + expect(translateUiText("Änderungen verwerfen", "en")).toBe("Discard changes"); + expect(translateUiText("Discard changes", "de")).toBe("Änderungen verwerfen"); + expect(translateUiText("Ungespeicherte Änderungen verworfen", "en")).toBe("Unsaved changes discarded"); + expect(translateUiText("Zwischenstand gespeichert – weitere Änderungen sind ungespeichert", "en")).toBe("Progress saved – additional changes remain unsaved"); expect(translateUiText("Passwort/Zugang", "en")).toBe("Password/access"); expect(translateUiText("Animationen", "en")).toBe("Animations"); expect(translateUiText("Animations", "de")).toBe("Animationen"); diff --git a/tests/settings-view.test.tsx b/tests/settings-view.test.tsx index 718ba07..f2a1575 100644 --- a/tests/settings-view.test.tsx +++ b/tests/settings-view.test.tsx @@ -4,7 +4,7 @@ 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, buildAccountCreateProviderOrderUpdate, createAccountDialogState, createSettingsDraft } from "../src/renderer/App"; +import { buildAccountAddFields, buildAccountCreateProviderOrderUpdate, createAccountDialogState, createDiscardedSettingsState, createSettingsDraft, resolveSettingsSaveCompletion } from "../src/renderer/App"; import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit"; import { buildScopedAccountEnabledState, @@ -342,10 +342,11 @@ function workspaceActions(overrides: Partial = {}): Acc }; } -function viewModel(saveState: SettingsViewModel["saveState"] = "clean"): SettingsViewModel { +function viewModel(saveState: SettingsViewModel["saveState"] = "clean", saveInFlight = false): SettingsViewModel { return { section: "accounts", saveState, + saveInFlight, form: formModel(), accounts: workspaceModel() }; @@ -354,6 +355,7 @@ function viewModel(saveState: SettingsViewModel["saveState"] = "clean"): Setting function viewActions(): SettingsViewActions { return { onSectionChange: () => {}, + onDiscard: () => {}, onSave: () => {}, form: { onChange: () => {}, onAction: () => {} }, accounts: workspaceActions() @@ -778,6 +780,37 @@ describe("settings views", () => { } }); + it("enables discarding only for unsaved or failed settings drafts", () => { + let discarded = 0; + const actions = { ...viewActions(), onDiscard: () => { discarded += 1; } }; + const findDiscard = (content: ReactElement): ReactElement<{ disabled?: boolean; onClick: () => void }> => { + let result: ReactElement<{ disabled?: boolean; onClick: () => void }> | null = null; + visitElements(content, (element) => { + if (element.type === "button" && element.props.children === "Änderungen verwerfen") { + result = element as ReactElement<{ disabled?: boolean; onClick: () => void }>; + } + }); + if (!result) throw new Error("Discard button missing"); + return result; + }; + + const dirty = findDiscard(SettingsContent({ actions, model: viewModel("dirty") })); + const clean = findDiscard(SettingsContent({ actions, model: viewModel("clean") })); + const saving = findDiscard(SettingsContent({ actions, model: viewModel("saving") })); + const saved = findDiscard(SettingsContent({ actions, model: viewModel("saved") })); + const error = findDiscard(SettingsContent({ actions, model: viewModel("error") })); + const dirtyWhileSaving = findDiscard(SettingsContent({ actions, model: viewModel("dirty", true) })); + + expect(dirty.props.disabled).toBe(false); + expect(clean.props.disabled).toBe(true); + expect(saving.props.disabled).toBe(true); + expect(saved.props.disabled).toBe(true); + expect(error.props.disabled).toBe(false); + expect(dirtyWhileSaving.props.disabled).toBe(true); + dirty.props.onClick(); + expect(discarded).toBe(1); + }); + it("renders the settings save action with a green background and black text", () => { const html = renderToStaticMarkup(); const css = readFileSync(new URL("../src/renderer/views/settings/settings.css", import.meta.url), "utf8"); @@ -1323,6 +1356,35 @@ describe("settings App integration", () => { expect(appSource).toContain("applyPersistedSettings(fresh.settings, false)"); }); + it("rebuilds every local settings input from the last persisted state", () => { + const persisted = createRendererSettings({ + ...defaultSettings(), + theme: "light", + speedLimitKbps: 12 * 1024, + bandwidthSchedules: [{ id: "night", startHour: 22, endHour: 6, speedLimitKbps: 8 * 1024, enabled: true }] + }); + + expect(createDiscardedSettingsState(persisted, "system")).toEqual({ + draft: { ...persisted, archivePasswordList: "", notifyUrl: "" }, + themeChoice: "system", + speedLimitInput: "12", + scheduleSpeedInputs: { night: "8" } + }); + }); + + it("does not apply a stale save result over newer draft changes", () => { + expect(resolveSettingsSaveCompletion(4, 4)).toEqual({ + saveState: "saved", + applyPersistedTheme: true, + toast: "Einstellungen gespeichert" + }); + expect(resolveSettingsSaveCompletion(4, 5)).toEqual({ + saveState: "dirty", + applyPersistedTheme: false, + toast: "Zwischenstand gespeichert – weitere Änderungen sind ungespeichert" + }); + }); + 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"); @@ -1429,9 +1491,20 @@ describe("settings App integration", () => { it("keeps specific persistence revision-safe when the draft changes in flight", () => { const block = sourceBlock(appSource, "const persistSpecificSettings", "const runAccountQuickAction"); + const toggleBlock = sourceBlock(appSource, "const persistAccountToggle", "const onToggleDebridLinkApiKeyEnabled"); expect(block).toContain("revisionAtStart"); expect(block).toContain("mergeConcurrentSpecificSettings"); expect(block).toContain('setSettingsSaveState("dirty")'); + expect(block).toContain("persistedSettingsRef.current = result"); + expect(block.indexOf("persistedSettingsRef.current = result")).toBeLessThan(block.indexOf("if (settingsDraftRevisionRef.current === revisionAtStart)")); + expect(block).toContain("themeChoiceAtStart: settingsThemeChoiceRef.current"); + expect(block).toContain("const { revisionAtStart, draftAtStart, themeChoiceAtStart } = persistenceContext"); + expect(block).toContain("persistedThemeChoiceRef.current = themeChoiceAtStart"); + expect(block.indexOf("persistedThemeChoiceRef.current = themeChoiceAtStart")).toBeLessThan(block.indexOf("if (settingsDraftRevisionRef.current === revisionAtStart)")); + expect(toggleBlock).toContain("const themeChoiceAtStart = settingsThemeChoiceRef.current"); + expect(toggleBlock).toContain("const persistenceContext = {"); + expect(toggleBlock.indexOf("const persistenceContext = {")).toBeLessThan(toggleBlock.indexOf("runAccountEnableRefresh(")); + expect(toggleBlock).toContain("persistSpecificSettings(nextDraft, persistenceContext)"); }); it("keeps unchecked single accounts honest without a positive status", () => {