feat(settings): discard unsaved changes

This commit is contained in:
Sucukdeluxe
2026-08-25 04:53:53 +02:00
parent 5a04a62945
commit bd2808a763
6 changed files with 244 additions and 43 deletions
+131 -26
View File
@@ -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<string, string>;
} {
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<SettingsSaveState, "saved" | "dirty">;
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<Record<string, string>>({});
const [settingsDirty, setSettingsDirty] = useState(false);
const [settingsSaveState, setSettingsSaveState] = useState<SettingsSaveState>("clean");
const [settingsSaveInFlight, setSettingsSaveInFlight] = useState(false);
const [schedulePickerOpen, setSchedulePickerOpen] = useState(false);
const [scheduleTimeInput, setScheduleTimeInput] = useState("");
const [scheduleStartDay, setScheduleStartDay] = useState<DailyScheduleStartDay>("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<UiSnapshot | null>(null);
const masterSnapshotRef = useRef<UiSnapshot | null>(null);
const snapshotRef = useRef(snapshot);
snapshotRef.current = snapshot;
const latestStateRef = useRef<UiSnapshot | null>(null);
const masterSnapshotRef = useRef<UiSnapshot | null>(null);
const snapshotRef = useRef(snapshot);
const persistedSettingsRef = useRef<RendererSettings>(emptySnapshot().settings);
const settingsThemeChoiceRef = useRef<SettingsThemeChoice>(settingsThemeChoice);
const persistedThemeChoiceRef = useRef<SettingsThemeChoice>(emptySnapshot().settings.theme);
snapshotRef.current = snapshot;
settingsThemeChoiceRef.current = settingsThemeChoice;
const tabRef = useRef(tab);
tabRef.current = tab;
const stateFlushTimerRef = useRef<ReturnType<typeof setTimeout> | 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<void> => {
@@ -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<RendererSettings> => {
const revisionAtStart = settingsDraftRevisionRef.current;
const draftAtStart = settingsDraft;
const persistSpecificSettings = async (
nextDraft: RendererSettingsDraft,
persistenceContext = {
revisionAtStart: settingsDraftRevisionRef.current,
draftAtStart: settingsDraft,
themeChoiceAtStart: settingsThemeChoiceRef.current
}
): Promise<RendererSettings> => {
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<RendererSettings> => {
const persistDraftSettings = async (themeChoiceAtStart: SettingsThemeChoice = settingsThemeChoiceRef.current): Promise<RendererSettings> => {
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
+1 -1
View File
@@ -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"],
+26 -14
View File
@@ -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 (
<section aria-label="Einstellungsbereich" className="settings-content settings-static">
<header className="settings-content-header">
@@ -70,12 +73,21 @@ export function SettingsContent({ model, actions }: SettingsViewProps): ReactEle
<h1>Einstellungen</h1>
<span aria-live="polite" className={`settings-save-state is-${model.saveState}`} role="status">{saveLabel}</span>
</div>
<button
className="settings-button settings-button-primary settings-save-button"
disabled={saveDisabled}
onClick={actions.onSave}
type="button"
>Einstellungen speichern</button>
<div className="settings-content-actions">
<button
className="settings-button settings-button-secondary settings-discard-button"
disabled={discardDisabled}
onClick={actions.onDiscard}
title="Stellt den letzten gespeicherten Stand wieder her."
type="button"
>Änderungen verwerfen</button>
<button
className="settings-button settings-button-primary settings-save-button"
disabled={saveDisabled}
onClick={actions.onSave}
type="button"
>Einstellungen speichern</button>
</div>
</header>
<div className={`settings-content-body${model.section === "accounts" ? " is-accounts" : ""}`}>
{model.section === "accounts"
+7
View File
@@ -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,
+4
View File
@@ -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");
+75 -2
View File
@@ -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<AccountWorkspaceActions> = {}): 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(<SettingsContent actions={viewActions()} model={viewModel("dirty")} />);
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", () => {