fix: harden and de-slop the merged sidebar/chat/settings batch
Post-merge follow-ups for #2740 #2735 #2734 #2690 #2676 #2738 #2684 #2689 #2733 #2739 #2462 #2687 #2736 #2618 #2697, plus three regressions found while reviewing them: - ctrl/cmd+digit while typing no longer switches session tabs (#2503 was still open in practice: the guard only covered the mod+alt surface binding) - Shiki template-call sanitizer now covers every bundled grammar, including the js/ts aliases and embedding grammars; timed-out highlight requests are memoized and no longer cancel unrelated in-flight requests - settings flush on suspend uses keepalive and also fires on Capacitor appStateChange; keeps the selected model persisted across mode switches - remote-only branches fetch before checkout; range helpers fail clearly - git status invalidation now fires for runtime adapters too - settings number inputs and select triggers size in ch so they scale with the interface font - recent-activity timestamps tick from one list-level ticker - Markdown preview find goes through the shared find_in_file keybind with containment, no longer counts its own bar, and debounces observer runs - #2676 reverted; #2524 fixed by fading the sticky header's own background instead of overlaying the content below it - sticky group headers in the model picker and sidebar render again (oc-sticky-fade-scroller class restored after9b9d7069c) - project switcher names are left-aligned again (wrapper lost in26dbc2f30) - tool card quick-open icon is always visible and opens the same line as the expanded card's button - tautological tests replaced or removed; new oxlint findings fixed
This commit is contained in:
@@ -914,6 +914,41 @@ describe('unload lifecycle flush (#2197)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('sends the unload flush with keepalive so the browser cannot cancel it', async () => {
|
||||
// No runtime settings API: the write has to take the HTTP branch, which is
|
||||
// the one the browser cancels on unload without `keepalive`.
|
||||
registerRuntimeAPIs(null);
|
||||
const inits: RequestInit[] = [];
|
||||
const previousFetch = globalThis.fetch;
|
||||
// SAFETY: the mock receives only the (input, init) pair production code
|
||||
// passes and always resolves to a Response; the assertion supplies the
|
||||
// overload signatures a plain arrow function cannot declare.
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
inits.push(init ?? {});
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const update = updateDesktopSettings({ gitChangesViewMode: 'flat' });
|
||||
getWindow().dispatchEvent(new Event('pagehide'));
|
||||
await update;
|
||||
await delay(50);
|
||||
|
||||
expect(inits).toHaveLength(1);
|
||||
expect(inits[0].method).toBe('PUT');
|
||||
expect(inits[0].keepalive).toBe(true);
|
||||
|
||||
// The ordinary debounced write stays a plain fetch.
|
||||
inits.length = 0;
|
||||
await updateDesktopSettings({ gitChangesViewMode: 'tree' });
|
||||
await delay(300);
|
||||
expect(inits).toHaveLength(1);
|
||||
expect(inits[0].keepalive).toBe(false);
|
||||
} finally {
|
||||
globalThis.fetch = previousFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('ignores lifecycle events when no settings write is pending', async () => {
|
||||
const saveCalls: Array<Partial<SettingsPayload>> = [];
|
||||
registerSettingsSave(async (changes) => {
|
||||
|
||||
Reference in New Issue
Block a user