mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-08-01 16:43:37 +08:00
feat(desktop): add BrowserSurface with bounds sync and address bar wiring
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
259391eb51
commit
0ac56a6b11
45
desktop/src/components/browser/BrowserSurface.test.tsx
Normal file
45
desktop/src/components/browser/BrowserSurface.test.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } as unknown as typeof ResizeObserver
|
||||||
|
})
|
||||||
|
|
||||||
|
const { bridge } = vi.hoisted(() => ({
|
||||||
|
bridge: { open: vi.fn(), navigate: vi.fn(), setBounds: vi.fn(), setVisible: vi.fn(), close: vi.fn() },
|
||||||
|
}))
|
||||||
|
vi.mock('../../lib/previewBridge', () => ({ previewBridge: bridge }))
|
||||||
|
|
||||||
|
import { BrowserSurface } from './BrowserSurface'
|
||||||
|
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Object.values(bridge).forEach((f) => f.mockReset())
|
||||||
|
useBrowserPanelStore.setState(useBrowserPanelStore.getInitialState(), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('BrowserSurface', () => {
|
||||||
|
it('opens the preview at the session url on mount when surface is open', () => {
|
||||||
|
useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
|
||||||
|
render(<BrowserSurface sessionId="s1" />)
|
||||||
|
expect(bridge.open).toHaveBeenCalledWith('http://localhost:5173/', expect.objectContaining({ width: expect.any(Number) }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('navigating via address bar calls store + bridge', () => {
|
||||||
|
useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
|
||||||
|
render(<BrowserSurface sessionId="s1" />)
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
fireEvent.change(input, { target: { value: 'http://localhost:3000/' } })
|
||||||
|
fireEvent.submit(input.closest('form')!)
|
||||||
|
expect(bridge.navigate).toHaveBeenCalledWith('http://localhost:3000/')
|
||||||
|
expect(useBrowserPanelStore.getState().bySession['s1']!.url).toBe('http://localhost:3000/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the native webview on unmount', () => {
|
||||||
|
useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
|
||||||
|
const { unmount } = render(<BrowserSurface sessionId="s1" />)
|
||||||
|
unmount()
|
||||||
|
expect(bridge.setVisible).toHaveBeenLastCalledWith(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
54
desktop/src/components/browser/BrowserSurface.tsx
Normal file
54
desktop/src/components/browser/BrowserSurface.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useEffect, useLayoutEffect, useRef } from 'react'
|
||||||
|
import { BrowserAddressBar } from './BrowserAddressBar'
|
||||||
|
import { computeWebviewBounds } from './computeWebviewBounds'
|
||||||
|
import { previewBridge } from '../../lib/previewBridge'
|
||||||
|
import { useBrowserPanelStore } from '../../stores/browserPanelStore'
|
||||||
|
|
||||||
|
export function BrowserSurface({ sessionId }: { sessionId: string }) {
|
||||||
|
const hostRef = useRef<HTMLDivElement>(null)
|
||||||
|
const session = useBrowserPanelStore((s) => s.bySession[sessionId])
|
||||||
|
const store = useBrowserPanelStore.getState()
|
||||||
|
|
||||||
|
const reportBounds = () => {
|
||||||
|
const el = hostRef.current
|
||||||
|
if (!el) return
|
||||||
|
previewBridge.setBounds(computeWebviewBounds(el.getBoundingClientRect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = hostRef.current
|
||||||
|
if (!el || !session) return
|
||||||
|
previewBridge.open(session.url, computeWebviewBounds(el.getBoundingClientRect()))
|
||||||
|
previewBridge.setVisible(true)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [sessionId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = hostRef.current
|
||||||
|
if (!el) return
|
||||||
|
const ro = new ResizeObserver(() => reportBounds())
|
||||||
|
ro.observe(el)
|
||||||
|
window.addEventListener('resize', reportBounds)
|
||||||
|
return () => { ro.disconnect(); window.removeEventListener('resize', reportBounds) }
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [sessionId])
|
||||||
|
|
||||||
|
useEffect(() => () => { previewBridge.setVisible(false) }, [])
|
||||||
|
|
||||||
|
if (!session) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<BrowserAddressBar
|
||||||
|
url={session.url}
|
||||||
|
canGoBack={session.canGoBack}
|
||||||
|
canGoForward={session.canGoForward}
|
||||||
|
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)}
|
||||||
|
/>
|
||||||
|
<div ref={hostRef} className="flex-1" data-testid="preview-host" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user