mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 23:22:05 +08:00
fix(minapps): replace webview ref with state
fix: Error: Cannot access refs during render
This commit is contained in:
parent
b5004e2a51
commit
d2c4231458
@ -88,7 +88,12 @@ const MinAppPage: FC = () => {
|
|||||||
|
|
||||||
// -------------- 新的 Tab Shell 逻辑 --------------
|
// -------------- 新的 Tab Shell 逻辑 --------------
|
||||||
// 注意:Hooks 必须在任何 return 之前调用,因此提前定义,并在内部判空
|
// 注意:Hooks 必须在任何 return 之前调用,因此提前定义,并在内部判空
|
||||||
const webviewRef = useRef<WebviewTag | null>(null)
|
const [webview, setWebview] = useState<WebviewTag | null>(null)
|
||||||
|
const webviewRef = useRef<WebviewTag | null>(webview)
|
||||||
|
useEffect(() => {
|
||||||
|
webviewRef.current = webview
|
||||||
|
}, [webview])
|
||||||
|
|
||||||
const [isReady, setIsReady] = useState<boolean>(() => (app ? getWebviewLoaded(app.id) : false))
|
const [isReady, setIsReady] = useState<boolean>(() => (app ? getWebviewLoaded(app.id) : false))
|
||||||
const [currentUrl, setCurrentUrl] = useState<string | null>(app?.url ?? null)
|
const [currentUrl, setCurrentUrl] = useState<string | null>(app?.url ?? null)
|
||||||
|
|
||||||
@ -103,7 +108,7 @@ const MinAppPage: FC = () => {
|
|||||||
|
|
||||||
if (webviewRef.current === el) return true // 已附着
|
if (webviewRef.current === el) return true // 已附着
|
||||||
|
|
||||||
webviewRef.current = el
|
setWebview(el)
|
||||||
const handleInPageNav = (e: any) => setCurrentUrl(e.url)
|
const handleInPageNav = (e: any) => setCurrentUrl(e.url)
|
||||||
el.addEventListener('did-navigate-in-page', handleInPageNav)
|
el.addEventListener('did-navigate-in-page', handleInPageNav)
|
||||||
webviewCleanupRef.current = () => {
|
webviewCleanupRef.current = () => {
|
||||||
@ -185,7 +190,7 @@ const MinAppPage: FC = () => {
|
|||||||
onOpenDevTools={handleOpenDevTools}
|
onOpenDevTools={handleOpenDevTools}
|
||||||
/>
|
/>
|
||||||
</ToolbarWrapper>
|
</ToolbarWrapper>
|
||||||
<WebviewSearch webviewRef={webviewRef} isWebviewReady={isReady} appId={app.id} />
|
<WebviewSearch activeWebview={webview} isWebviewReady={isReady} appId={app.id} />
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<LoadingMask>
|
<LoadingMask>
|
||||||
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />
|
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />
|
||||||
|
|||||||
@ -8,14 +8,14 @@ import { useTranslation } from 'react-i18next'
|
|||||||
type FoundInPageResult = Electron.FoundInPageResult
|
type FoundInPageResult = Electron.FoundInPageResult
|
||||||
|
|
||||||
interface WebviewSearchProps {
|
interface WebviewSearchProps {
|
||||||
webviewRef: React.RefObject<WebviewTag | null>
|
activeWebview: WebviewTag | null
|
||||||
isWebviewReady: boolean
|
isWebviewReady: boolean
|
||||||
appId: string
|
appId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = loggerService.withContext('WebviewSearch')
|
const logger = loggerService.withContext('WebviewSearch')
|
||||||
|
|
||||||
const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, appId }) => {
|
const WebviewSearch: FC<WebviewSearchProps> = ({ activeWebview, isWebviewReady, appId }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
@ -25,7 +25,6 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
const focusFrameRef = useRef<number | null>(null)
|
const focusFrameRef = useRef<number | null>(null)
|
||||||
const lastAppIdRef = useRef<string>(appId)
|
const lastAppIdRef = useRef<string>(appId)
|
||||||
const attachedWebviewRef = useRef<WebviewTag | null>(null)
|
const attachedWebviewRef = useRef<WebviewTag | null>(null)
|
||||||
const activeWebview = webviewRef.current ?? null
|
|
||||||
|
|
||||||
const focusInput = useCallback(() => {
|
const focusInput = useCallback(() => {
|
||||||
if (focusFrameRef.current !== null) {
|
if (focusFrameRef.current !== null) {
|
||||||
@ -81,7 +80,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getUsableWebview = useCallback(() => {
|
const getUsableWebview = useCallback(() => {
|
||||||
const candidates = [webviewRef.current, attachedWebviewRef.current]
|
const candidates = [activeWebview, attachedWebviewRef.current]
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const usable = ensureWebviewReady(candidate)
|
const usable = ensureWebviewReady(candidate)
|
||||||
@ -91,7 +90,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}, [ensureWebviewReady, webviewRef])
|
}, [ensureWebviewReady, activeWebview])
|
||||||
|
|
||||||
const stopSearch = useCallback(() => {
|
const stopSearch = useCallback(() => {
|
||||||
const target = getUsableWebview()
|
const target = getUsableWebview()
|
||||||
|
|||||||
@ -136,9 +136,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('opens the search overlay with keyboard shortcut', async () => {
|
it('opens the search overlay with keyboard shortcut', async () => {
|
||||||
const { webview } = createWebviewMock()
|
const { webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument()
|
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument()
|
||||||
|
|
||||||
@ -149,9 +148,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('opens the search overlay when webview shortcut is forwarded', async () => {
|
it('opens the search overlay when webview shortcut is forwarded', async () => {
|
||||||
const { webview } = createWebviewMock()
|
const { webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
@ -170,13 +168,12 @@ describe('WebviewSearch', () => {
|
|||||||
;(webview as any).getWebContentsId = vi.fn(() => {
|
;(webview as any).getWebContentsId = vi.fn(() => {
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
|
|
||||||
const getWebContentsIdMock = vi.fn(() => {
|
const getWebContentsIdMock = vi.fn(() => {
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
;(webview as any).getWebContentsId = getWebContentsIdMock
|
;(webview as any).getWebContentsId = getWebContentsIdMock
|
||||||
const { rerender } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
const { rerender } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getWebContentsIdMock).toHaveBeenCalled()
|
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||||
@ -185,8 +182,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
;(webview as any).getWebContentsId = vi.fn(() => 1)
|
;(webview as any).getWebContentsId = vi.fn(() => 1)
|
||||||
|
|
||||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
rerender(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
|
||||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
rerender(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
@ -200,9 +197,8 @@ describe('WebviewSearch', () => {
|
|||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
;(webview as any).getWebContentsId = getWebContentsIdMock
|
;(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" />)
|
const { rerender, unmount } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getWebContentsIdMock).toHaveBeenCalled()
|
expect(getWebContentsIdMock).toHaveBeenCalled()
|
||||||
@ -212,7 +208,7 @@ describe('WebviewSearch', () => {
|
|||||||
throw new Error('should not be called')
|
throw new Error('should not be called')
|
||||||
})
|
})
|
||||||
|
|
||||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
rerender(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
|
||||||
expect(stopFindInPageMock).not.toHaveBeenCalled()
|
expect(stopFindInPageMock).not.toHaveBeenCalled()
|
||||||
|
|
||||||
unmount()
|
unmount()
|
||||||
@ -221,9 +217,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
@ -245,10 +240,9 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('performs searches and navigates between results', async () => {
|
it('performs searches and navigates between results', async () => {
|
||||||
const { emit, findInPageMock, webview } = createWebviewMock()
|
const { emit, findInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
await openSearchOverlay()
|
await openSearchOverlay()
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByRole('textbox')
|
||||||
@ -286,10 +280,9 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('navigates results when enter is forwarded from the webview', async () => {
|
it('navigates results when enter is forwarded from the webview', async () => {
|
||||||
const { findInPageMock, webview } = createWebviewMock()
|
const { findInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onFindShortcutMock).toHaveBeenCalled()
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
@ -325,10 +318,9 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('clears search state when appId changes', async () => {
|
it('clears search state when appId changes', async () => {
|
||||||
const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock()
|
const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
const { rerender } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
const { rerender } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
await openSearchOverlay()
|
await openSearchOverlay()
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByRole('textbox')
|
||||||
@ -338,7 +330,7 @@ describe('WebviewSearch', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-2" />)
|
rerender(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-2" />)
|
||||||
})
|
})
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -352,10 +344,9 @@ describe('WebviewSearch', () => {
|
|||||||
findInPageMock.mockImplementation(() => {
|
findInPageMock.mockImplementation(() => {
|
||||||
throw new Error('findInPage failed')
|
throw new Error('findInPage failed')
|
||||||
})
|
})
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
await openSearchOverlay()
|
await openSearchOverlay()
|
||||||
|
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByRole('textbox')
|
||||||
@ -368,9 +359,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('stops search when component unmounts', async () => {
|
it('stops search when component unmounts', async () => {
|
||||||
const { stopFindInPageMock, webview } = createWebviewMock()
|
const { stopFindInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
|
|
||||||
const { unmount } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
const { unmount } = render(<WebviewSearch activeWebview={webview} isWebviewReady appId="app-1" />)
|
||||||
await openSearchOverlay()
|
await openSearchOverlay()
|
||||||
|
|
||||||
stopFindInPageMock.mockClear()
|
stopFindInPageMock.mockClear()
|
||||||
@ -382,9 +372,8 @@ describe('WebviewSearch', () => {
|
|||||||
|
|
||||||
it('ignores keyboard shortcut when webview is not ready', async () => {
|
it('ignores keyboard shortcut when webview is not ready', async () => {
|
||||||
const { findInPageMock, webview } = createWebviewMock()
|
const { findInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
|
||||||
|
|
||||||
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
|
render(<WebviewSearch activeWebview={webview} isWebviewReady={false} appId="app-1" />)
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.keyDown(window, { key: 'f', ctrlKey: true })
|
fireEvent.keyDown(window, { key: 'f', ctrlKey: true })
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user