import { act, cleanup, render, waitFor } from '@testing-library/react' import { useEffect, useRef } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { assistantTextPart, type ChatMessage } from '@/lib/chat-messages' import { $previewTarget, clearSessionPreviewRegistry, type PreviewTarget, registerSessionPreview } from '@/store/preview' import { $currentCwd, $messages } from '@/store/session' import type { RpcEvent } from '@/types/hermes' import { usePreviewRouting } from './use-preview-routing' function assistantMessage(id: string, text: string): ChatMessage { return { id, parts: [assistantTextPart(text)], role: 'assistant' } } function previewTarget(source: string): PreviewTarget { const isUrl = /^https?:\/\//i.test(source) return { kind: isUrl ? 'url' : 'file', label: source, path: isUrl ? undefined : source, previewKind: isUrl ? undefined : 'html', source, url: isUrl ? source : `file://${source}` } } let handleEvent: (event: RpcEvent) => void = () => undefined function PreviewRoutingHarness({ onEvent }: { onEvent: (handler: (event: RpcEvent) => void) => void }) { const activeSessionIdRef = useRef('session-1') const routing = usePreviewRouting({ activeSessionIdRef, baseHandleGatewayEvent: vi.fn(), currentCwd: '/work', currentView: 'chat', requestGateway: vi.fn(), routedSessionId: 'session-1', selectedStoredSessionId: null }) useEffect(() => { onEvent(routing.handleDesktopGatewayEvent) }, [onEvent, routing.handleDesktopGatewayEvent]) return null } describe('usePreviewRouting', () => { beforeEach(() => { $currentCwd.set('/work') $messages.set([]) $previewTarget.set(null) window.localStorage.clear() clearSessionPreviewRegistry() handleEvent = () => undefined Object.defineProperty(window, 'hermesDesktop', { configurable: true, value: { normalizePreviewTarget: vi.fn(async (target: string) => previewTarget(target)) } }) }) afterEach(() => { cleanup() $messages.set([]) $previewTarget.set(null) window.localStorage.clear() clearSessionPreviewRegistry() vi.restoreAllMocks() }) it('opens the active session preview from the registry', async () => { const target = previewTarget('/work/demo.html') registerSessionPreview('session-1', target, 'tool-result') render( { handleEvent = handler }} /> ) await waitFor(() => { expect($previewTarget.get()).toEqual({ ...target, renderMode: 'preview' }) }) }) it('does not infer previews from assistant prose', async () => { render( { handleEvent = handler }} /> ) act(() => { $messages.set([ assistantMessage('a1', 'Preview: http://localhost:5173/'), assistantMessage('a2', 'Open /work/demo.html') ]) }) expect($previewTarget.get()).toBeNull() expect(window.hermesDesktop.normalizePreviewTarget).not.toHaveBeenCalled() }) it('registers structured tool-result preview targets', async () => { render( { handleEvent = handler }} /> ) act(() => handleEvent({ payload: { path: './dist/index.html' }, session_id: 'session-1', type: 'tool.complete' }) ) await waitFor(() => { expect($previewTarget.get()?.source).toBe('./dist/index.html') }) expect(window.localStorage.getItem('hermes.desktop.sessionPreviews.v1')).toContain('./dist/index.html') }) it('registers html previews from edit inline diffs', async () => { render( { handleEvent = handler }} /> ) act(() => handleEvent({ payload: { inline_diff: '\u001b[38;2;218;165;32ma/preview-demo.html -> b/preview-demo.html\u001b[0m\n' }, session_id: 'session-1', type: 'tool.complete' }) ) await waitFor(() => { expect($previewTarget.get()?.source).toBe('preview-demo.html') }) }) })