diff --git a/apps/desktop/src/views/SearchView/composables/useModelDropdownPopup.ts b/apps/desktop/src/views/SearchView/composables/useModelDropdownPopup.ts index 40b03872..58d3043d 100644 --- a/apps/desktop/src/views/SearchView/composables/useModelDropdownPopup.ts +++ b/apps/desktop/src/views/SearchView/composables/useModelDropdownPopup.ts @@ -90,6 +90,19 @@ export function useModelDropdownPopup(options: UseModelDropdownPopupOptions) { popupSessionVersion: parsePopupSessionVersion(popupId), } : null; + + if (disposed) { + if (identity) { + await popupManager.hide(identity).catch((error) => { + console.error( + '[SearchView] Failed to hide model dropdown popup on unmount:', + error + ); + }); + } + return; + } + const isLivePopupSession = identity ? isLiveModelDropdownPopupSession(identity) : false; if (!identity || !isLivePopupSession) { @@ -204,12 +217,29 @@ export function useModelDropdownPopup(options: UseModelDropdownPopupOptions) { onUnmounted(() => { disposed = true; + const closingIdentity = + activePopupId !== null + ? { + popupId: activePopupId, + windowLabel: 'popup-model-dropdown-popup', + popupSessionVersion: parsePopupSessionVersion(activePopupId), + } + : null; + const shouldHidePopup = hasActivePopupSession || isOpen.value; cleanupFn?.(); cleanupFn = null; activePopupId = null; hasActivePopupSession = false; isOpen.value = false; onPopupSessionEnd?.(); + + if (!shouldHidePopup || !closingIdentity) { + return; + } + + void popupManager.hide(closingIdentity).catch((error) => { + console.error('[SearchView] Failed to hide model dropdown popup on unmount:', error); + }); }); /** diff --git a/apps/desktop/tests/composables/SearchView/useModelDropdownPopup.test.ts b/apps/desktop/tests/composables/SearchView/useModelDropdownPopup.test.ts index 2a02409e..de9912b5 100644 --- a/apps/desktop/tests/composables/SearchView/useModelDropdownPopup.test.ts +++ b/apps/desktop/tests/composables/SearchView/useModelDropdownPopup.test.ts @@ -216,6 +216,65 @@ describe('useModelDropdownPopup', () => { mounted.unmount(); }); + it('hides the current popup session on unmount when the popup is still open', async () => { + const onPopupSessionEnd = vi.fn(); + const mounted = await mountComposable(() => + useModelDropdownPopup({ + getAnchorElement: () => document.createElement('button'), + getPopupData: () => createPopupData(), + isModelDropdownActive: () => true, + onModelSelect: () => undefined, + onModelSearchQueryChange: () => undefined, + onClose: () => undefined, + onPopupSessionEnd, + }) + ); + + await mounted.result.open(); + mounted.unmount(); + + expect(popupManager.hide).toHaveBeenCalledWith({ + popupId: 'popup-model-dropdown-popup:1', + popupSessionVersion: 1, + windowLabel: 'popup-model-dropdown-popup', + }); + expect(onPopupSessionEnd).toHaveBeenCalledTimes(1); + }); + + it('closes a popup that finishes opening after the composable unmounts', async () => { + let resolveShow: ((popupId: string) => void) | undefined; + vi.mocked(popupManager.show).mockImplementation( + () => + new Promise((resolve) => { + resolveShow = resolve; + }) + ); + const onPopupSessionStart = vi.fn(); + const mounted = await mountComposable(() => + useModelDropdownPopup({ + getAnchorElement: () => document.createElement('button'), + getPopupData: () => createPopupData(), + isModelDropdownActive: () => true, + onModelSelect: () => undefined, + onModelSearchQueryChange: () => undefined, + onClose: () => undefined, + onPopupSessionStart, + }) + ); + + const opening = mounted.result.open(); + mounted.unmount(); + resolveShow?.('popup-model-dropdown-popup:1'); + await opening; + + expect(popupManager.hide).toHaveBeenCalledWith({ + popupId: 'popup-model-dropdown-popup:1', + windowLabel: 'popup-model-dropdown-popup', + popupSessionVersion: expect.any(Number), + }); + expect(onPopupSessionStart).not.toHaveBeenCalled(); + }); + it('closes the active popup session with the current identity', async () => { const onPopupSessionEnd = vi.fn(); const mounted = await mountComposable(() =>