fix(settings): flush pending debounced settings writes on page unload (#2734)

fix(settings): flush pending debounced settings writes on page unload
This commit is contained in:
Bohdan Triapitsyn
2026-08-28 23:41:07 +03:00
committed by GitHub
3 changed files with 116 additions and 1 deletions
+84
View File
@@ -844,3 +844,87 @@ describe('updateDesktopSettings', () => {
expect(saveCalls.some((changes) => changes.autoSaveEnabled === true)).toBe(true);
});
});
describe('unload lifecycle flush (#2197)', () => {
beforeEach(() => {
getWindow();
registerRuntimeAPIs(null);
invalidateSettingsCache();
});
test('flushes a pending debounced settings save on pagehide without a double write', async () => {
const saveCalls: Array<Partial<SettingsPayload>> = [];
registerSettingsSave(async (changes) => {
saveCalls.push(changes);
return {};
});
const update = updateDesktopSettings({ showDeletionDialog: false });
expect(saveCalls).toEqual([]);
getWindow().dispatchEvent(new Event('pagehide'));
// The flush must hand the pending changes to the settings backend
// synchronously inside the lifecycle listener — an unloading window has
// no later turn for the debounce timer.
expect(saveCalls).toEqual([{ showDeletionDialog: false }]);
await update;
await delay(300);
// The canceled debounce timer must not replay the same write.
expect(saveCalls).toHaveLength(1);
});
test('flushes a pending debounced settings save on beforeunload without a double write', async () => {
const saveCalls: Array<Partial<SettingsPayload>> = [];
registerSettingsSave(async (changes) => {
saveCalls.push(changes);
return {};
});
const update = updateDesktopSettings({ gitChangesViewMode: 'tree' });
expect(saveCalls).toEqual([]);
getWindow().dispatchEvent(new Event('beforeunload'));
expect(saveCalls).toEqual([{ gitChangesViewMode: 'tree' }]);
await update;
await delay(300);
expect(saveCalls).toHaveLength(1);
});
test('persists a showDeletionDialog toggle followed by an immediate unload', async () => {
const saveCalls: Array<Partial<SettingsPayload>> = [];
registerSettingsSave(async (changes) => {
saveCalls.push(changes);
return {};
});
startAppearanceAutoSave();
try {
useUIStore.getState().setShowDeletionDialog(false);
getWindow().dispatchEvent(new Event('pagehide'));
expect(saveCalls.some((changes) => changes.showDeletionDialog === false)).toBe(true);
} finally {
useUIStore.getState().setShowDeletionDialog(true);
// Let the restore write drain so it cannot leak into other tests.
await delay(300);
}
});
test('ignores lifecycle events when no settings write is pending', async () => {
const saveCalls: Array<Partial<SettingsPayload>> = [];
registerSettingsSave(async (changes) => {
saveCalls.push(changes);
return {};
});
getWindow().dispatchEvent(new Event('pagehide'));
getWindow().dispatchEvent(new Event('beforeunload'));
await delay(50);
expect(saveCalls).toEqual([]);
});
});