diff --git a/src/renderer/src/pages/minapps/MinAppPage.tsx b/src/renderer/src/pages/minapps/MinAppPage.tsx index 75c6c284c2..3da51c0c6b 100644 --- a/src/renderer/src/pages/minapps/MinAppPage.tsx +++ b/src/renderer/src/pages/minapps/MinAppPage.tsx @@ -88,7 +88,12 @@ const MinAppPage: FC = () => { // -------------- 新的 Tab Shell 逻辑 -------------- // 注意:Hooks 必须在任何 return 之前调用,因此提前定义,并在内部判空 - const webviewRef = useRef(null) + const [webview, setWebview] = useState(null) + const webviewRef = useRef(webview) + useEffect(() => { + webviewRef.current = webview + }, [webview]) + const [isReady, setIsReady] = useState(() => (app ? getWebviewLoaded(app.id) : false)) const [currentUrl, setCurrentUrl] = useState(app?.url ?? null) @@ -103,7 +108,7 @@ const MinAppPage: FC = () => { if (webviewRef.current === el) return true // 已附着 - webviewRef.current = el + setWebview(el) const handleInPageNav = (e: any) => setCurrentUrl(e.url) el.addEventListener('did-navigate-in-page', handleInPageNav) webviewCleanupRef.current = () => { @@ -185,7 +190,7 @@ const MinAppPage: FC = () => { onOpenDevTools={handleOpenDevTools} /> - + {!isReady && ( diff --git a/src/renderer/src/pages/minapps/components/WebviewSearch.tsx b/src/renderer/src/pages/minapps/components/WebviewSearch.tsx index a5340ee2c0..ba3f77d8b4 100644 --- a/src/renderer/src/pages/minapps/components/WebviewSearch.tsx +++ b/src/renderer/src/pages/minapps/components/WebviewSearch.tsx @@ -8,14 +8,14 @@ import { useTranslation } from 'react-i18next' type FoundInPageResult = Electron.FoundInPageResult interface WebviewSearchProps { - webviewRef: React.RefObject + activeWebview: WebviewTag | null isWebviewReady: boolean appId: string } const logger = loggerService.withContext('WebviewSearch') -const WebviewSearch: FC = ({ webviewRef, isWebviewReady, appId }) => { +const WebviewSearch: FC = ({ activeWebview, isWebviewReady, appId }) => { const { t } = useTranslation() const [isVisible, setIsVisible] = useState(false) const [query, setQuery] = useState('') @@ -25,7 +25,6 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app const focusFrameRef = useRef(null) const lastAppIdRef = useRef(appId) const attachedWebviewRef = useRef(null) - const activeWebview = webviewRef.current ?? null const focusInput = useCallback(() => { if (focusFrameRef.current !== null) { @@ -81,7 +80,7 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app ) const getUsableWebview = useCallback(() => { - const candidates = [webviewRef.current, attachedWebviewRef.current] + const candidates = [activeWebview, attachedWebviewRef.current] for (const candidate of candidates) { const usable = ensureWebviewReady(candidate) @@ -91,7 +90,7 @@ const WebviewSearch: FC = ({ webviewRef, isWebviewReady, app } return null - }, [ensureWebviewReady, webviewRef]) + }, [ensureWebviewReady, activeWebview]) const stopSearch = useCallback(() => { const target = getUsableWebview() 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 4deee62ad5..c7c0573b2c 100644 --- a/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx +++ b/src/renderer/src/pages/minapps/components/__tests__/WebviewSearch.test.tsx @@ -136,9 +136,8 @@ describe('WebviewSearch', () => { it('opens the search overlay with keyboard shortcut', async () => { const { webview } = createWebviewMock() - const webviewRef = { current: webview } as React.RefObject - render() + render() expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument() @@ -149,9 +148,8 @@ describe('WebviewSearch', () => { it('opens the search overlay when webview shortcut is forwarded', async () => { const { webview } = createWebviewMock() - const webviewRef = { current: webview } as React.RefObject - render() + render() await waitFor(() => { expect(onFindShortcutMock).toHaveBeenCalled() @@ -170,13 +168,12 @@ describe('WebviewSearch', () => { ;(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() + const { rerender } = render() await waitFor(() => { expect(getWebContentsIdMock).toHaveBeenCalled() @@ -185,8 +182,8 @@ describe('WebviewSearch', () => { ;(webview as any).getWebContentsId = vi.fn(() => 1) - rerender() - rerender() + rerender() + rerender() await waitFor(() => { expect(onFindShortcutMock).toHaveBeenCalled() @@ -200,9 +197,8 @@ describe('WebviewSearch', () => { throw error }) ;(webview as any).getWebContentsId = getWebContentsIdMock - const webviewRef = { current: webview } as React.RefObject - const { rerender, unmount } = render() + const { rerender, unmount } = render() await waitFor(() => { expect(getWebContentsIdMock).toHaveBeenCalled() @@ -212,7 +208,7 @@ describe('WebviewSearch', () => { throw new Error('should not be called') }) - rerender() + rerender() expect(stopFindInPageMock).not.toHaveBeenCalled() unmount() @@ -221,9 +217,8 @@ describe('WebviewSearch', () => { it('closes the search overlay when escape is forwarded from the webview', async () => { const { webview } = createWebviewMock() - const webviewRef = { current: webview } as React.RefObject - render() + render() await waitFor(() => { expect(onFindShortcutMock).toHaveBeenCalled() @@ -245,10 +240,9 @@ describe('WebviewSearch', () => { it('performs searches and navigates between results', async () => { const { emit, findInPageMock, webview } = createWebviewMock() - const webviewRef = { current: webview } as React.RefObject const user = userEvent.setup() - render() + render() await openSearchOverlay() const input = screen.getByRole('textbox') @@ -286,10 +280,9 @@ describe('WebviewSearch', () => { it('navigates results when enter is forwarded from the webview', async () => { const { findInPageMock, webview } = createWebviewMock() - const webviewRef = { current: webview } as React.RefObject const user = userEvent.setup() - render() + render() await waitFor(() => { expect(onFindShortcutMock).toHaveBeenCalled() @@ -325,10 +318,9 @@ describe('WebviewSearch', () => { it('clears search state when appId changes', async () => { const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock() - const webviewRef = { current: webview } as React.RefObject const user = userEvent.setup() - const { rerender } = render() + const { rerender } = render() await openSearchOverlay() const input = screen.getByRole('textbox') @@ -338,7 +330,7 @@ describe('WebviewSearch', () => { }) await act(async () => { - rerender() + rerender() }) await waitFor(() => { @@ -352,10 +344,9 @@ describe('WebviewSearch', () => { findInPageMock.mockImplementation(() => { throw new Error('findInPage failed') }) - const webviewRef = { current: webview } as React.RefObject const user = userEvent.setup() - render() + render() await openSearchOverlay() const input = screen.getByRole('textbox') @@ -368,9 +359,8 @@ describe('WebviewSearch', () => { it('stops search when component unmounts', async () => { const { stopFindInPageMock, webview } = createWebviewMock() - const webviewRef = { current: webview } as React.RefObject - const { unmount } = render() + const { unmount } = render() await openSearchOverlay() stopFindInPageMock.mockClear() @@ -382,9 +372,8 @@ describe('WebviewSearch', () => { it('ignores keyboard shortcut when webview is not ready', async () => { const { findInPageMock, webview } = createWebviewMock() - const webviewRef = { current: webview } as React.RefObject - render() + render() await act(async () => { fireEvent.keyDown(window, { key: 'f', ctrlKey: true })