mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 22:10:21 +08:00
fix: guard webview search against destroyed webviews (#10704)
* 🐛 fix: guard webview search against destroyed webviews
* delete code
* delete code
This commit is contained in:
parent
bb003c071c
commit
45195bb57d
@ -46,15 +46,58 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
setActiveIndex(0)
|
setActiveIndex(0)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const stopSearch = useCallback(() => {
|
const ensureWebviewReady = useCallback(
|
||||||
const target = webviewRef.current ?? attachedWebviewRef.current
|
(candidate: WebviewTag | null) => {
|
||||||
if (!target) return
|
if (!candidate) return null
|
||||||
try {
|
try {
|
||||||
target.stopFindInPage('clearSelection')
|
const webContentsId = candidate.getWebContentsId?.()
|
||||||
} catch (error) {
|
if (!webContentsId) {
|
||||||
logger.error('stopFindInPage failed', { error })
|
logger.debug('WebviewSearch: missing webContentsId before action', { appId })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('WebviewSearch: getWebContentsId failed before action', { appId, error })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate
|
||||||
|
},
|
||||||
|
[appId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const stopFindOnWebview = useCallback(
|
||||||
|
(webview: WebviewTag | null) => {
|
||||||
|
const usable = ensureWebviewReady(webview)
|
||||||
|
if (!usable) return false
|
||||||
|
try {
|
||||||
|
usable.stopFindInPage('clearSelection')
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('stopFindInPage failed', { appId, error })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[appId, ensureWebviewReady]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getUsableWebview = useCallback(() => {
|
||||||
|
const candidates = [webviewRef.current, attachedWebviewRef.current]
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const usable = ensureWebviewReady(candidate)
|
||||||
|
if (usable) {
|
||||||
|
return usable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [webviewRef])
|
|
||||||
|
return null
|
||||||
|
}, [ensureWebviewReady, webviewRef])
|
||||||
|
|
||||||
|
const stopSearch = useCallback(() => {
|
||||||
|
const target = getUsableWebview()
|
||||||
|
if (!target) return
|
||||||
|
stopFindOnWebview(target)
|
||||||
|
}, [getUsableWebview, stopFindOnWebview])
|
||||||
|
|
||||||
const closeSearch = useCallback(() => {
|
const closeSearch = useCallback(() => {
|
||||||
setIsVisible(false)
|
setIsVisible(false)
|
||||||
@ -64,7 +107,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
|
|
||||||
const performSearch = useCallback(
|
const performSearch = useCallback(
|
||||||
(text: string, options?: Electron.FindInPageOptions) => {
|
(text: string, options?: Electron.FindInPageOptions) => {
|
||||||
const target = webviewRef.current ?? attachedWebviewRef.current
|
const target = getUsableWebview()
|
||||||
if (!target) {
|
if (!target) {
|
||||||
logger.debug('Skip performSearch: webview not attached')
|
logger.debug('Skip performSearch: webview not attached')
|
||||||
return
|
return
|
||||||
@ -81,7 +124,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
window.toast?.error(t('common.error'))
|
window.toast?.error(t('common.error'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[resetSearchState, stopSearch, t, webviewRef]
|
[getUsableWebview, resetSearchState, stopSearch, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFoundInPage = useCallback((event: Event & { result?: FoundInPageResult }) => {
|
const handleFoundInPage = useCallback((event: Event & { result?: FoundInPageResult }) => {
|
||||||
@ -129,22 +172,26 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
return () => {
|
return () => {
|
||||||
activeWebview.removeEventListener('found-in-page', handle)
|
activeWebview.removeEventListener('found-in-page', handle)
|
||||||
if (attachedWebviewRef.current === activeWebview) {
|
if (attachedWebviewRef.current === activeWebview) {
|
||||||
try {
|
stopFindOnWebview(activeWebview)
|
||||||
activeWebview.stopFindInPage('clearSelection')
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('stopFindInPage failed', { error })
|
|
||||||
}
|
|
||||||
attachedWebviewRef.current = null
|
attachedWebviewRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [activeWebview, handleFoundInPage])
|
}, [activeWebview, handleFoundInPage, stopFindOnWebview])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeWebview) return
|
if (!activeWebview) return
|
||||||
|
if (!isWebviewReady) return
|
||||||
const onFindShortcut = window.api?.webview?.onFindShortcut
|
const onFindShortcut = window.api?.webview?.onFindShortcut
|
||||||
if (!onFindShortcut) return
|
if (!onFindShortcut) return
|
||||||
|
|
||||||
const webContentsId = activeWebview.getWebContentsId?.()
|
let webContentsId: number | undefined
|
||||||
|
try {
|
||||||
|
webContentsId = activeWebview.getWebContentsId?.()
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('WebviewSearch: getWebContentsId failed', { appId, error })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!webContentsId) {
|
if (!webContentsId) {
|
||||||
logger.warn('WebviewSearch: missing webContentsId', { appId })
|
logger.warn('WebviewSearch: missing webContentsId', { appId })
|
||||||
return
|
return
|
||||||
@ -177,7 +224,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe?.()
|
unsubscribe?.()
|
||||||
}
|
}
|
||||||
}, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, openSearch])
|
}, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, isWebviewReady, openSearch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVisible) return
|
if (!isVisible) return
|
||||||
|
|||||||
@ -164,6 +164,61 @@ describe('WebviewSearch', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('skips shortcut wiring when getWebContentsId throws', async () => {
|
||||||
|
const { webview } = createWebviewMock()
|
||||||
|
const error = new Error('not ready')
|
||||||
|
;(webview as any).getWebContentsId = vi.fn(() => {
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||||
|
|
||||||
|
const getWebContentsIdMock = vi.fn(() => {
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
;(webview as any).getWebContentsId = getWebContentsIdMock
|
||||||
|
const { rerender } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
expect(onFindShortcutMock).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
;(webview as any).getWebContentsId = vi.fn(() => 1)
|
||||||
|
|
||||||
|
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
||||||
|
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not call stopFindInPage when webview is not ready', async () => {
|
||||||
|
const { stopFindInPageMock, webview } = createWebviewMock()
|
||||||
|
const error = new Error('loading')
|
||||||
|
const getWebContentsIdMock = vi.fn(() => {
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
;(webview as any).getWebContentsId = getWebContentsIdMock
|
||||||
|
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||||
|
|
||||||
|
const { rerender, unmount } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
stopFindInPageMock.mockImplementation(() => {
|
||||||
|
throw new Error('should not be called')
|
||||||
|
})
|
||||||
|
|
||||||
|
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
||||||
|
expect(stopFindInPageMock).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
unmount()
|
||||||
|
expect(stopFindInPageMock).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('closes the search overlay when escape is forwarded from the webview', async () => {
|
it('closes the search overlay when escape is forwarded from the webview', async () => {
|
||||||
const { webview } = createWebviewMock()
|
const { webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user