From fc5924f1f1fbfb98f55a9ab2da5f980e80f66a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Thu, 4 Jun 2026 16:40:06 +0800 Subject: [PATCH] fix(desktop): close preview on renderer reload (#680) Close the native preview WebContentsView when the Electron renderer starts a top-level navigation so refreshed session pages cannot leave the in-app browser surface behind. Fixes #680 Tested: - bun run verify Confidence: high Scope-risk: narrow --- desktop/electron/main.ts | 4 ++ .../services/previewLifecycle.test.ts | 68 +++++++++++++++++++ desktop/electron/services/previewLifecycle.ts | 42 ++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 desktop/electron/services/previewLifecycle.test.ts create mode 100644 desktop/electron/services/previewLifecycle.ts diff --git a/desktop/electron/main.ts b/desktop/electron/main.ts index cb20b5da..7928028c 100644 --- a/desktop/electron/main.ts +++ b/desktop/electron/main.ts @@ -28,6 +28,7 @@ import { import { installMacOsChromiumKeychainPromptGuard } from './services/keychain' import { applyWindowsAppUserModelId } from './services/appIdentity' import { installMainWindowNavigationGuards, installPreviewNavigationGuards } from './services/navigationGuards' +import { installPreviewCleanupOnRendererNavigation } from './services/previewLifecycle' import { logNotificationSmokeRendererAck, scheduleNotificationSmoke } from './services/notificationSmoke' import { normalizeZoomFactor } from './services/zoom' import { resolveRendererEntry } from './services/rendererEntry' @@ -316,6 +317,9 @@ async function createMainWindow() { }) installMainWindowNavigationGuards(mainWindow.webContents, { openExternal: openExternalUrl }) + installPreviewCleanupOnRendererNavigation(mainWindow.webContents, () => { + previewService?.close() + }) installWindowLifecycle({ app, diff --git a/desktop/electron/services/previewLifecycle.test.ts b/desktop/electron/services/previewLifecycle.test.ts new file mode 100644 index 00000000..b61ea27f --- /dev/null +++ b/desktop/electron/services/previewLifecycle.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it, vi } from 'vitest' +import { + installPreviewCleanupOnRendererNavigation, + type PreviewCleanupWebContents, +} from './previewLifecycle' + +type DidStartNavigationHandler = Parameters[1] + +class FakeMainWebContents { + didStartNavigationHandler: DidStartNavigationHandler | null = null + + on(event: 'did-start-navigation', handler: DidStartNavigationHandler) { + if (event === 'did-start-navigation') { + this.didStartNavigationHandler = handler + } + } + + emitDidStartNavigation(input: { isSameDocument?: boolean, isMainFrame?: boolean } = {}) { + this.didStartNavigationHandler?.( + { + isSameDocument: input.isSameDocument ?? false, + isMainFrame: input.isMainFrame ?? true, + }, + ) + } + + emitDeprecatedDidStartNavigation(input: { url?: string, isInPlace?: boolean, isMainFrame?: boolean } = {}) { + this.didStartNavigationHandler?.( + {}, + input.url ?? 'app://renderer', + input.isInPlace, + input.isMainFrame ?? true, + ) + } +} + +describe('preview lifecycle cleanup', () => { + it('closes the native preview view before the renderer performs a top-level reload', () => { + const webContents = new FakeMainWebContents() + const closePreview = vi.fn() + + installPreviewCleanupOnRendererNavigation(webContents, closePreview) + webContents.emitDidStartNavigation({ isMainFrame: true, isSameDocument: false }) + + expect(closePreview).toHaveBeenCalledTimes(1) + }) + + it('keeps the native preview view for same-document or subframe navigation', () => { + const webContents = new FakeMainWebContents() + const closePreview = vi.fn() + + installPreviewCleanupOnRendererNavigation(webContents, closePreview) + webContents.emitDidStartNavigation({ isMainFrame: true, isSameDocument: true }) + webContents.emitDidStartNavigation({ isMainFrame: false, isSameDocument: false }) + + expect(closePreview).not.toHaveBeenCalled() + }) + + it('falls back to deprecated navigation booleans when Electron details are absent', () => { + const webContents = new FakeMainWebContents() + const closePreview = vi.fn() + + installPreviewCleanupOnRendererNavigation(webContents, closePreview) + webContents.emitDeprecatedDidStartNavigation({ isMainFrame: true, isInPlace: false }) + + expect(closePreview).toHaveBeenCalledTimes(1) + }) +}) diff --git a/desktop/electron/services/previewLifecycle.ts b/desktop/electron/services/previewLifecycle.ts new file mode 100644 index 00000000..42f2397d --- /dev/null +++ b/desktop/electron/services/previewLifecycle.ts @@ -0,0 +1,42 @@ +type NavigationDetails = { + isSameDocument?: boolean + isMainFrame?: boolean +} + +export type PreviewCleanupWebContents = { + on( + event: 'did-start-navigation', + handler: ( + details: NavigationDetails, + url?: string, + isInPlace?: boolean, + isMainFrame?: boolean, + ) => void, + ): unknown +} + +function isMainFrameNavigation( + details: NavigationDetails, + deprecatedIsMainFrame?: boolean, +) { + return details.isMainFrame ?? deprecatedIsMainFrame === true +} + +function isSameDocumentNavigation( + details: NavigationDetails, + deprecatedIsInPlace?: boolean, +) { + return details.isSameDocument ?? deprecatedIsInPlace === true +} + +export function installPreviewCleanupOnRendererNavigation( + webContents: PreviewCleanupWebContents, + closePreview: () => void, +): void { + webContents.on('did-start-navigation', (details, _url, isInPlace, isMainFrame) => { + if (!isMainFrameNavigation(details, isMainFrame)) return + if (isSameDocumentNavigation(details, isInPlace)) return + + closePreview() + }) +}