import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { useState } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AgentBrowserPanel } from '@/pages/Chat/AgentBrowserPanel'; import type { AgentBrowserSnapshot } from '../../shared/agent-browser'; const hostApiFetchMock = vi.hoisted(() => vi.fn()); const hostEventListeners = vi.hoisted( () => new Map void>(), ); vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), })); vi.mock('@/lib/host-events', () => ({ subscribeHostEvent: (eventName: string, handler: (payload: unknown) => void) => { hostEventListeners.set(eventName, handler); return () => hostEventListeners.delete(eventName); }, })); class MockResizeObserver { constructor(private readonly callback: ResizeObserverCallback) {} observe(element: Element) { this.callback([{ target: element } as ResizeObserverEntry], this as unknown as ResizeObserver); } disconnect() {} unobserve() {} } function snapshot(overrides: Partial = {}): AgentBrowserSnapshot { return { browserId: 'browser-a', projectId: 'project-a', projectPath: 'D:/repo', state: 'attached', generation: 1, url: 'http://127.0.0.1:4173/', title: 'App', visible: false, bounds: null, canGoBack: false, canGoForward: false, eventCursor: 0, ...overrides, }; } function body(init?: RequestInit): Record { return JSON.parse(String(init?.body ?? '{}')) as Record; } function Harness({ initialOpen = false }: { initialOpen?: boolean }) { const [open, setOpen] = useState(initialOpen); return ( ); } describe('AgentBrowserPanel', () => { beforeEach(() => { hostApiFetchMock.mockReset(); hostEventListeners.clear(); Object.defineProperty(globalThis, 'ResizeObserver', { configurable: true, value: MockResizeObserver, }); vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({ x: 600, y: 80, left: 600, top: 80, right: 1000, bottom: 680, width: 400, height: 600, toJSON: () => ({}), } as DOMRect); hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => { if (path.startsWith('/api/agent-browser/state?')) { return { success: true, browser: snapshot() }; } if (path === '/api/agent-browser/present') { return { success: true, browser: snapshot({ visible: body(init).visible === true, bounds: body(init).bounds as AgentBrowserSnapshot['bounds'], }), }; } if (path === '/api/agent-browser/diagnostics') { return { success: true, browser: snapshot({ visible: true }) }; } if (path === '/api/agent-browser/close') { return { success: true, browser: snapshot({ browserId: null, state: 'closed', visible: false }), }; } if (path === '/api/agent-browser/cdp/events') { return { success: true, page: { events: [], nextCursor: 0, hasMore: false } }; } throw new Error(`Unexpected request: ${path}`); }); }); afterEach(() => { vi.restoreAllMocks(); }); it('does no browser API work while closed and presents an agent-opened snapshot', async () => { render(); expect(screen.queryByTestId('agent-browser-panel')).not.toBeInTheDocument(); expect(hostApiFetchMock).not.toHaveBeenCalled(); act(() => { hostEventListeners.get('agent-browser:show')?.(snapshot()); }); expect(await screen.findByTestId('agent-browser-panel')).toBeInTheDocument(); await waitFor(() => { expect(hostApiFetchMock).toHaveBeenCalledWith( '/api/agent-browser/present', expect.anything(), ); }); const presentation = hostApiFetchMock.mock.calls.find( ([path, init]) => path === '/api/agent-browser/present' && body(init as RequestInit).visible === true, ); expect(body(presentation?.[1] as RequestInit)).toMatchObject({ visible: true, bounds: { x: 600, y: 80, width: 400, height: 600 }, }); }); it('enables Renderer diagnostics only after the drawer is explicitly expanded', async () => { render(); await screen.findByTestId('agent-browser-panel'); await waitFor(() => expect(hostApiFetchMock.mock.calls.some( ([path]) => String(path).startsWith('/api/agent-browser/state?'), )).toBe(true)); expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/agent-browser/diagnostics')).toBe(false); fireEvent.click(screen.getByRole('button', { name: '展开调试面板' })); await waitFor(() => { const request = hostApiFetchMock.mock.calls.find(([path]) => path === '/api/agent-browser/diagnostics'); expect(body(request?.[1] as RequestInit)).toMatchObject({ enabled: true }); }); }); it('refreshes browser state once when the app regains focus', async () => { render(); await waitFor(() => expect(hostApiFetchMock.mock.calls.filter( ([path]) => String(path).startsWith('/api/agent-browser/state?'), )).toHaveLength(1)); hostApiFetchMock.mockClear(); act(() => window.dispatchEvent(new Event('focus'))); await waitFor(() => expect(hostApiFetchMock.mock.calls.filter( ([path]) => String(path).startsWith('/api/agent-browser/state?'), )).toHaveLength(1)); }); });