diff --git a/src/renderer/src/pages/minapps/components/WebviewSearch.tsx b/src/renderer/src/pages/minapps/components/WebviewSearch.tsx index bff5605c1c..a5340ee2c0 100644 --- a/src/renderer/src/pages/minapps/components/WebviewSearch.tsx +++ b/src/renderer/src/pages/minapps/components/WebviewSearch.tsx @@ -46,15 +46,58 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app setActiveIndex(0) }, []) - const stopSearch = useCallback(() => { - const target = webviewRef.current ?? attachedWebviewRef.current - if (!target) return - try { - target.stopFindInPage('clearSelection') - } catch (error) { - logger.error('stopFindInPage failed', { error }) + const ensureWebviewReady = useCallback( + (candidate: WebviewTag | null) => { + if (!candidate) return null + try { + const webContentsId = candidate.getWebContentsId?.() + if (!webContentsId) { + 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(() => { setIsVisible(false) @@ -64,7 +107,7 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app const performSearch = useCallback( (text: string, options?: Electron.FindInPageOptions) => { - const target = webviewRef.current ?? attachedWebviewRef.current + const target = getUsableWebview() if (!target) { logger.debug('Skip performSearch: webview not attached') return @@ -81,7 +124,7 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app window.toast?.error(t('common.error')) } }, - [resetSearchState, stopSearch, t, webviewRef] + [getUsableWebview, resetSearchState, stopSearch, t] ) const handleFoundInPage = useCallback((event: Event & { result?: FoundInPageResult }) => { @@ -129,22 +172,26 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app return () => { activeWebview.removeEventListener('found-in-page', handle) if (attachedWebviewRef.current === activeWebview) { - try { - activeWebview.stopFindInPage('clearSelection') - } catch (error) { - logger.error('stopFindInPage failed', { error }) - } + stopFindOnWebview(activeWebview) attachedWebviewRef.current = null } } - }, [activeWebview, handleFoundInPage]) + }, [activeWebview, handleFoundInPage, stopFindOnWebview]) useEffect(() => { if (!activeWebview) return + if (!isWebviewReady) return const onFindShortcut = window.api?.webview?.onFindShortcut 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) { logger.warn('WebviewSearch: missing webContentsId', { appId }) return @@ -177,7 +224,7 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app return () => { unsubscribe?.() } - }, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, openSearch]) + }, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, isWebviewReady, openSearch]) useEffect(() => { if (!isVisible) return diff --git a/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx b/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx index 50e8c60ca2..4deee62ad5 100644 --- a/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx +++ b/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx @@ -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 + + const getWebContentsIdMock = vi.fn(() => { + throw error + }) + ;(webview as any).getWebContentsId = getWebContentsIdMock + const { rerender } = render() + + await waitFor(() => { + expect(getWebContentsIdMock).toHaveBeenCalled() + }) + expect(onFindShortcutMock).not.toHaveBeenCalled() + + ;(webview as any).getWebContentsId = vi.fn(() => 1) + + rerender() + rerender() + + 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 + + const { rerender, unmount } = render() + + await waitFor(() => { + expect(getWebContentsIdMock).toHaveBeenCalled() + }) + + stopFindInPageMock.mockImplementation(() => { + throw new Error('should not be called') + }) + + rerender() + expect(stopFindInPageMock).not.toHaveBeenCalled() + + unmount() + expect(stopFindInPageMock).not.toHaveBeenCalled() + }) + it('closes the search overlay when escape is forwarded from the webview', async () => { const { webview } = createWebviewMock() const webviewRef = { current: webview } as React.RefObject