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
This commit is contained in:
程序员阿江(Relakkes) 2026-06-04 16:40:06 +08:00
parent c6a674003f
commit fc5924f1f1
3 changed files with 114 additions and 0 deletions

View File

@ -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,

View File

@ -0,0 +1,68 @@
import { describe, expect, it, vi } from 'vitest'
import {
installPreviewCleanupOnRendererNavigation,
type PreviewCleanupWebContents,
} from './previewLifecycle'
type DidStartNavigationHandler = Parameters<PreviewCleanupWebContents['on']>[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)
})
})

View File

@ -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()
})
}