mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-26 15:03:34 +08:00
fix(desktop): route typed local html in browser panel
Tested: - cd desktop && bun run test -- src/components/browser/BrowserAddressBar.test.tsx src/components/browser/BrowserSurface.test.tsx --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1 - bun run check:desktop
This commit is contained in:
parent
b70f25e62b
commit
f9575b9749
@ -48,6 +48,20 @@ describe('BrowserAddressBar', () => {
|
|||||||
expect(onNavigate).toHaveBeenNthCalledWith(2, 'https://example.com')
|
expect(onNavigate).toHaveBeenNthCalledWith(2, 'https://example.com')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('preserves typed local html paths for the browser surface to route', () => {
|
||||||
|
const onNavigate = vi.fn()
|
||||||
|
render(<BrowserAddressBar {...baseProps} url="" onNavigate={onNavigate} />)
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
|
||||||
|
fireEvent.change(input, { target: { value: '/private/tmp/report.html' } })
|
||||||
|
fireEvent.submit(input.closest('form')!)
|
||||||
|
fireEvent.change(input, { target: { value: 'out/index.html' } })
|
||||||
|
fireEvent.submit(input.closest('form')!)
|
||||||
|
|
||||||
|
expect(onNavigate).toHaveBeenNthCalledWith(1, '/private/tmp/report.html')
|
||||||
|
expect(onNavigate).toHaveBeenNthCalledWith(2, 'out/index.html')
|
||||||
|
})
|
||||||
|
|
||||||
it('preserves explicit URL schemes', () => {
|
it('preserves explicit URL schemes', () => {
|
||||||
const onNavigate = vi.fn()
|
const onNavigate = vi.fn()
|
||||||
render(<BrowserAddressBar {...baseProps} url="" onNavigate={onNavigate} />)
|
render(<BrowserAddressBar {...baseProps} url="" onNavigate={onNavigate} />)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { ArrowLeft, ArrowRight, Loader2, RotateCw } from 'lucide-react'
|
import { ArrowLeft, ArrowRight, Loader2, RotateCw } from 'lucide-react'
|
||||||
|
import { isHtmlFilePath } from '../../lib/htmlPreviewPolicy'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@ -58,6 +59,7 @@ export function normalizeBrowserAddress(input: string): string {
|
|||||||
const value = input.trim()
|
const value = input.trim()
|
||||||
if (!value) return ''
|
if (!value) return ''
|
||||||
if (/^[a-z][a-z\d+\-.]*:\/\//i.test(value) || /^(about|data|file):/i.test(value)) return value
|
if (/^[a-z][a-z\d+\-.]*:\/\//i.test(value) || /^(about|data|file):/i.test(value)) return value
|
||||||
|
if (isHtmlFilePath(value)) return value
|
||||||
if (/^(localhost|127(?:\.\d{1,3}){3}|\[::1\]|::1)(?::\d+)?(?:[/?#].*)?$/i.test(value)) {
|
if (/^(localhost|127(?:\.\d{1,3}){3}|\[::1\]|::1)(?::\d+)?(?:[/?#].*)?$/i.test(value)) {
|
||||||
return `http://${value}`
|
return `http://${value}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ vi.mock('../../lib/previewBridge', () => ({ previewBridge: bridge }))
|
|||||||
vi.mock('@tauri-apps/api/event', () => ({ listen: () => Promise.resolve(() => {}) }))
|
vi.mock('@tauri-apps/api/event', () => ({ listen: () => Promise.resolve(() => {}) }))
|
||||||
|
|
||||||
import { BrowserSurface } from './BrowserSurface'
|
import { BrowserSurface } from './BrowserSurface'
|
||||||
|
import { getDefaultBaseUrl, setBaseUrl } from '../../api/client'
|
||||||
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
|
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
|
||||||
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
|
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
|
||||||
import { useOverlayStore } from '../../stores/overlayStore'
|
import { useOverlayStore } from '../../stores/overlayStore'
|
||||||
@ -28,6 +29,7 @@ afterEach(() => {
|
|||||||
// browserPanelStore.open() now also opens the unified workbench; keep it isolated.
|
// browserPanelStore.open() now also opens the unified workbench; keep it isolated.
|
||||||
useWorkspacePanelStore.setState(useWorkspacePanelStore.getInitialState(), true)
|
useWorkspacePanelStore.setState(useWorkspacePanelStore.getInitialState(), true)
|
||||||
useOverlayStore.setState(useOverlayStore.getInitialState(), true)
|
useOverlayStore.setState(useOverlayStore.getInitialState(), true)
|
||||||
|
setBaseUrl(getDefaultBaseUrl())
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('BrowserSurface', () => {
|
describe('BrowserSurface', () => {
|
||||||
@ -76,6 +78,27 @@ describe('BrowserSurface', () => {
|
|||||||
expect(bridge.navigate).not.toHaveBeenCalled()
|
expect(bridge.navigate).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('opens a typed file URL for local html through the local-file preview route', async () => {
|
||||||
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 }))
|
||||||
|
setBaseUrl('http://127.0.0.1:8787')
|
||||||
|
useBrowserPanelStore.getState().ensureBlank('s1')
|
||||||
|
render(<BrowserSurface sessionId="s1" />)
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
fireEvent.change(input, { target: { value: 'file:///private/tmp/report.html' } })
|
||||||
|
fireEvent.submit(input.closest('form')!)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(bridge.open).toHaveBeenCalledWith(
|
||||||
|
'http://127.0.0.1:8787/local-file/private/tmp/report.html',
|
||||||
|
expect.objectContaining({ width: expect.any(Number) }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
expect(useBrowserPanelStore.getState().bySession['s1']!.url).toBe(
|
||||||
|
'http://127.0.0.1:8787/local-file/private/tmp/report.html',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('navigating via address bar calls store + bridge', () => {
|
it('navigating via address bar calls store + bridge', () => {
|
||||||
useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
|
useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
|
||||||
render(<BrowserSurface sessionId="s1" />)
|
render(<BrowserSurface sessionId="s1" />)
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import { useEffect, useLayoutEffect, useRef } from 'react'
|
|||||||
import { Camera, Loader2, MousePointer2 } from 'lucide-react'
|
import { Camera, Loader2, MousePointer2 } from 'lucide-react'
|
||||||
import { BrowserAddressBar } from './BrowserAddressBar'
|
import { BrowserAddressBar } from './BrowserAddressBar'
|
||||||
import { computeWebviewBounds } from './computeWebviewBounds'
|
import { computeWebviewBounds } from './computeWebviewBounds'
|
||||||
import { isLoopbackHostname } from '../../lib/desktopRuntime'
|
import { getServerBaseUrl, isLoopbackHostname } from '../../lib/desktopRuntime'
|
||||||
|
import { classifyPreviewLink } from '../../lib/previewLinkRouter'
|
||||||
|
import { isAbsoluteLocalPath, localFileUrl, previewFsUrl } from '../../lib/handlePreviewLink'
|
||||||
import { previewBridge } from '../../lib/previewBridge'
|
import { previewBridge } from '../../lib/previewBridge'
|
||||||
import { subscribePreviewEvents } from '../../lib/previewEvents'
|
import { subscribePreviewEvents } from '../../lib/previewEvents'
|
||||||
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
|
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
|
||||||
@ -38,6 +40,21 @@ async function waitForLocalPreview(url: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveBrowserNavigationUrl(input: string, sessionId: string): string {
|
||||||
|
const value = input.trim()
|
||||||
|
if (!value) return ''
|
||||||
|
|
||||||
|
const classified = classifyPreviewLink(value)
|
||||||
|
if (classified.kind === 'browser-file' && classified.path) {
|
||||||
|
const serverBaseUrl = getServerBaseUrl()
|
||||||
|
return isAbsoluteLocalPath(classified.path)
|
||||||
|
? localFileUrl(serverBaseUrl, classified.path)
|
||||||
|
: previewFsUrl(serverBaseUrl, sessionId, classified.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
export function BrowserSurface({ sessionId }: { sessionId: string }) {
|
export function BrowserSurface({ sessionId }: { sessionId: string }) {
|
||||||
const hostRef = useRef<HTMLDivElement>(null)
|
const hostRef = useRef<HTMLDivElement>(null)
|
||||||
const loadSeqRef = useRef(0)
|
const loadSeqRef = useRef(0)
|
||||||
@ -126,7 +143,8 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
|
|||||||
|
|
||||||
if (!session) return null
|
if (!session) return null
|
||||||
|
|
||||||
const openOrNavigate = (url: string) => {
|
const openOrNavigate = (inputUrl: string) => {
|
||||||
|
const url = resolveBrowserNavigationUrl(inputUrl, sessionId)
|
||||||
if (!url) return
|
if (!url) return
|
||||||
const current = useBrowserPanelStore.getState().bySession[sessionId]
|
const current = useBrowserPanelStore.getState().bySession[sessionId]
|
||||||
store.navigate(sessionId, url)
|
store.navigate(sessionId, url)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user