diff --git a/desktop/src/components/browser/BrowserAddressBar.test.tsx b/desktop/src/components/browser/BrowserAddressBar.test.tsx
index c82beed7..e6c6cc3a 100644
--- a/desktop/src/components/browser/BrowserAddressBar.test.tsx
+++ b/desktop/src/components/browser/BrowserAddressBar.test.tsx
@@ -34,4 +34,16 @@ describe('BrowserAddressBar', () => {
fireEvent.click(screen.getByLabelText('后退'))
expect(onBack).toHaveBeenCalled()
})
+
+ it('shows the loading progress bar and a busy reload button while loading', () => {
+ render()
+ expect(screen.getByTestId('browser-loading-bar')).toBeInTheDocument()
+ expect(screen.getByLabelText('刷新')).toHaveAttribute('aria-busy', 'true')
+ })
+
+ it('hides the loading progress bar when not loading', () => {
+ render()
+ expect(screen.queryByTestId('browser-loading-bar')).not.toBeInTheDocument()
+ expect(screen.getByLabelText('刷新')).toHaveAttribute('aria-busy', 'false')
+ })
})
diff --git a/desktop/src/components/browser/BrowserAddressBar.tsx b/desktop/src/components/browser/BrowserAddressBar.tsx
index 37776bc6..05b34095 100644
--- a/desktop/src/components/browser/BrowserAddressBar.tsx
+++ b/desktop/src/components/browser/BrowserAddressBar.tsx
@@ -1,25 +1,28 @@
import { useEffect, useState } from 'react'
-import { ArrowLeft, ArrowRight, RotateCw } from 'lucide-react'
+import { ArrowLeft, ArrowRight, Loader2, RotateCw } from 'lucide-react'
type Props = {
url: string
canGoBack: boolean
canGoForward: boolean
+ loading?: boolean
onNavigate: (url: string) => void
onBack: () => void
onForward: () => void
onReload: () => void
}
-export function BrowserAddressBar({ url, canGoBack, canGoForward, onNavigate, onBack, onForward, onReload }: Props) {
+export function BrowserAddressBar({ url, canGoBack, canGoForward, loading = false, onNavigate, onBack, onForward, onReload }: Props) {
const [draft, setDraft] = useState(url)
useEffect(() => { setDraft(url) }, [url])
return (
-
+
-
+
+ {loading && (
+
+ )}
)
}
diff --git a/desktop/src/components/browser/BrowserSurface.test.tsx b/desktop/src/components/browser/BrowserSurface.test.tsx
index ae4baa80..9d829608 100644
--- a/desktop/src/components/browser/BrowserSurface.test.tsx
+++ b/desktop/src/components/browser/BrowserSurface.test.tsx
@@ -61,4 +61,43 @@ describe('BrowserSurface', () => {
expect(useBrowserPanelStore.getState().bySession['s1']!.pickerActive).toBe(false)
expect(bridge.eval).toHaveBeenLastCalledWith(expect.stringContaining('exit-picker'))
})
+
+ it('renders the loading indicator while the session is loading (open starts loading)', () => {
+ useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
+ render(
)
+ expect(screen.getByTestId('browser-loading-bar')).toBeInTheDocument()
+ expect(screen.getByLabelText('刷新')).toHaveAttribute('aria-busy', 'true')
+ })
+
+ it('hides the loading indicator once the page is ready', () => {
+ useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
+ useBrowserPanelStore.getState().setReady('s1')
+ render(
)
+ expect(screen.queryByTestId('browser-loading-bar')).not.toBeInTheDocument()
+ expect(screen.getByLabelText('刷新')).toHaveAttribute('aria-busy', 'false')
+ })
+
+ it('reload flips the session back into loading and shows the indicator', () => {
+ useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
+ useBrowserPanelStore.getState().setReady('s1')
+ render(
)
+ expect(screen.queryByTestId('browser-loading-bar')).not.toBeInTheDocument()
+ fireEvent.click(screen.getByLabelText('刷新'))
+ expect(useBrowserPanelStore.getState().bySession['s1']!.loading).toBe(true)
+ expect(bridge.navigate).toHaveBeenCalledWith('http://localhost:5173/')
+ expect(screen.getByTestId('browser-loading-bar')).toBeInTheDocument()
+ })
+
+ it('forces loading off after the timeout fallback elapses', () => {
+ vi.useFakeTimers()
+ try {
+ useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
+ render(
)
+ expect(useBrowserPanelStore.getState().bySession['s1']!.loading).toBe(true)
+ vi.advanceTimersByTime(15000)
+ expect(useBrowserPanelStore.getState().bySession['s1']!.loading).toBe(false)
+ } finally {
+ vi.useRealTimers()
+ }
+ })
})
diff --git a/desktop/src/components/browser/BrowserSurface.tsx b/desktop/src/components/browser/BrowserSurface.tsx
index ef55a1c9..0ab181cb 100644
--- a/desktop/src/components/browser/BrowserSurface.tsx
+++ b/desktop/src/components/browser/BrowserSurface.tsx
@@ -44,6 +44,17 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
useEffect(() => () => { previewBridge.setVisible(false) }, [])
+ // 兜底:navigated/ready 依赖注入脚本,若外站 CSP 拦截则永不回灌。loading 变 true 后 ~15s 强制收尾。
+ const isLoading = session?.loading ?? false
+ const currentUrl = session?.url
+ useEffect(() => {
+ if (!isLoading) return
+ const timer = window.setTimeout(() => {
+ useBrowserPanelStore.getState().setLoading(sessionId, false)
+ }, 15000)
+ return () => window.clearTimeout(timer)
+ }, [isLoading, currentUrl, sessionId])
+
if (!session) return null
return (
@@ -52,10 +63,11 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
url={session.url}
canGoBack={session.canGoBack}
canGoForward={session.canGoForward}
+ loading={session.loading}
onNavigate={(url) => { store.navigate(sessionId, url); previewBridge.navigate(url) }}
- onBack={() => { store.goBack(sessionId); previewBridge.navigate(useBrowserPanelStore.getState().bySession[sessionId]!.url) }}
- onForward={() => { store.goForward(sessionId); previewBridge.navigate(useBrowserPanelStore.getState().bySession[sessionId]!.url) }}
- onReload={() => previewBridge.navigate(session.url)}
+ onBack={() => { store.goBack(sessionId); store.setLoading(sessionId, true); previewBridge.navigate(useBrowserPanelStore.getState().bySession[sessionId]!.url) }}
+ onForward={() => { store.goForward(sessionId); store.setLoading(sessionId, true); previewBridge.navigate(useBrowserPanelStore.getState().bySession[sessionId]!.url) }}
+ onReload={() => { store.setLoading(sessionId, true); previewBridge.navigate(session.url) }}
/>