// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { PreviewPanel } from '@/components/Session/PreviewPanel'; import { registerPreviewWebview, unregisterPreviewWebview, } from '@/components/Session/PreviewPanel/tabs/browser/webviewRegistry'; import { HostProvider } from '@/host'; import { getSessionPreviewSlice, usePageTabStore } from '@/store/pageTabStore'; import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('@/components/Folder/FilePreview', () => ({ FilePreview: ({ file }: { file: FileInfo | null }) => (
{file?.name || 'No file selected'}
), })); // React Flow needs layout APIs jsdom lacks; the canvas tab is exercised // elsewhere, so stub it to keep this suite focused on the router/tab strip. vi.mock('@/components/Session/PreviewPanel/tabs/CanvasTab', () => ({ CanvasTab: () =>
, })); // xterm needs real layout/canvas APIs; the terminal tab has its own suite. vi.mock('@/components/Session/PreviewPanel/tabs/terminal/TerminalTab', () => ({ TerminalTab: () =>
, })); // Monaco and the live project stores are covered by the ReviewTab suite. vi.mock('@/components/Session/PreviewPanel/tabs/ReviewTab', () => ({ ReviewTab: () =>
, })); // The chooser lists the project's agent terminal streams via this hook; keep // the suite off the chat-store dependency chain and drive it with fixtures. let mockTerminalSources: Array<{ id: string; agentName: string; taskLabel: string; lines: string[]; status: 'running' | 'idle'; }> = []; vi.mock( '@/components/Session/PreviewPanel/tabs/terminal/useSessionTerminalSources', () => ({ useSessionTerminalSources: () => mockTerminalSources, }) ); // The desktop host is detected by electronAPI presence; embedded browsing // itself is -tag based and driven through the webview registry. const openExternal = vi.fn(); const host = { ipcRenderer: null, electronAPI: { openExternal } }; function renderPanel() { return render( ); } function previewSlice() { return getSessionPreviewSlice(usePageTabStore.getState()); } function activeType() { const slice = previewSlice(); return slice.tabs.find((tab) => tab.id === slice.activeTabId)?.type; } describe('PreviewPanel', () => { beforeEach(() => { mockTerminalSources = []; openExternal.mockReset(); openExternal.mockResolvedValue({ success: true }); usePageTabStore.setState({ sessionPreviewProjectId: null, sessionPreviewByProject: {}, previewBrowserViewport: null, }); usePageTabStore.getState().setSessionPreviewProject('project-test'); usePageTabStore.getState().toggleSessionPreview(); }); it('opens on the chooser tab listing the available content kinds', () => { renderPanel(); expect(screen.getByRole('tab', { name: 'New tab' })).toBeInTheDocument(); // Vertical options use the same product copy users see in the chooser. for (const label of ['Browser', 'Files', 'Review', 'Terminal']) { expect( screen.getByRole('button', { name: new RegExp(`^${label}\\b`), }) ).toBeInTheDocument(); } // Reserved kinds stay hidden from the chooser until a later version. for (const label of ['Canvas']) { expect( screen.queryByRole('button', { name: new RegExp(`^${label}\\b`), }) ).not.toBeInTheDocument(); } }); it('lists the project’s agent streams in the chooser and opens one in place', async () => { const user = userEvent.setup(); mockTerminalSources = [ { id: 'chat-1:turn-1:sub-1', agentName: 'Developer Agent', taskLabel: 'Start dev server', lines: ['npm run dev'], status: 'running', }, ]; renderPanel(); expect(screen.getByText('From this project')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: /Start dev server/ })); const slice = previewSlice(); const active = slice.tabs.find((tab) => tab.id === slice.activeTabId); expect(active).toMatchObject({ type: 'terminal', title: 'Developer Agent', agentSourceId: 'chat-1:turn-1:sub-1', }); // The chooser was converted in place, not left behind. expect(slice.tabs).toHaveLength(1); expect(screen.getByTestId('terminal-tab')).toBeInTheDocument(); }); it('picking a chooser option turns the tab into that content kind', async () => { const user = userEvent.setup(); renderPanel(); await user.click(screen.getByRole('button', { name: /^Browser\b/ })); expect(activeType()).toBe('browser'); // Address bar of the browser tab is now shown. expect( screen.getByRole('textbox', { name: 'Enter a URL' }) ).toBeInTheDocument(); }); it('routes to the file tab and reuses its tab by path', () => { const file = { name: 'notes.md', path: '/tmp/notes.md' } as FileInfo; usePageTabStore.getState().openFilePreview(file); usePageTabStore.getState().openFilePreview({ ...file }); renderPanel(); expect(screen.getByTestId('file-preview')).toHaveTextContent('notes.md'); expect(screen.getAllByRole('tab', { name: 'notes.md' })).toHaveLength(1); }); it('routes review, terminal, and canvas tabs to their surfaces', () => { const store = usePageTabStore.getState(); const chooserId = previewSlice().tabs[0].id; act(() => store.choosePreviewTabType(chooserId, 'review')); const { rerender } = renderPanel(); expect(screen.getByTestId('review-tab')).toBeInTheDocument(); act(() => store.choosePreviewTabType(previewSlice().activeTabId!, 'terminal') ); rerender( ); expect(screen.getByTestId('terminal-tab')).toBeInTheDocument(); act(() => store.choosePreviewTabType(previewSlice().activeTabId!, 'canvas') ); rerender( ); expect(screen.getByTestId('canvas-tab')).toBeInTheDocument(); }); it('the + button adds a new chooser tab', async () => { const user = userEvent.setup(); renderPanel(); await user.click(screen.getByRole('button', { name: 'New tab' })); expect(screen.getAllByRole('tab', { name: 'New tab' })).toHaveLength(2); expect(activeType()).toBe('chooser'); }); it('drives back/forward/reload on the registered guest element', async () => { const user = userEvent.setup(); const store = usePageTabStore.getState(); const chooserId = previewSlice().tabs[0].id; act(() => store.choosePreviewTabType(chooserId, 'browser')); const browserTab = previewSlice().tabs.find( (tab) => tab.type === 'browser' )!; act(() => store.updateBrowserPreviewTab(browserTab.id, { url: 'https://example.com/a', navigation: { url: 'https://example.com/a', title: 'A', isLoading: false, canGoBack: true, canGoForward: true, }, }) ); const goBack = vi.fn(); const goForward = vi.fn(); const reload = vi.fn(); registerPreviewWebview(browserTab.webviewId, { goBack, goForward, reload, } as unknown as HTMLElement & { goBack: typeof goBack }); try { renderPanel(); await user.click(screen.getByRole('button', { name: 'Back' })); await user.click(screen.getByRole('button', { name: 'Forward' })); await user.click(screen.getByRole('button', { name: 'Reload' })); expect(goBack).toHaveBeenCalled(); expect(goForward).toHaveBeenCalled(); expect(reload).toHaveBeenCalled(); } finally { unregisterPreviewWebview(browserTab.webviewId); } }); it('opens desktop external links through the Electron external IPC', async () => { const user = userEvent.setup(); const store = usePageTabStore.getState(); const chooserId = previewSlice().tabs[0].id; act(() => store.choosePreviewTabType(chooserId, 'browser')); const browserTab = previewSlice().tabs.find( (tab) => tab.type === 'browser' )!; act(() => store.updateBrowserPreviewTab(browserTab.id, { url: 'http://localhost:3000/', }) ); renderPanel(); await user.click(screen.getByRole('button', { name: 'Open externally' })); expect(openExternal).toHaveBeenCalledWith('http://localhost:3000/'); }); it('closing the final tab closes the panel', async () => { const user = userEvent.setup(); renderPanel(); await user.click(screen.getByRole('button', { name: 'Close tab' })); expect(previewSlice()).toMatchObject({ open: false, tabs: [], activeTabId: null, }); }); });