mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-19 21:53:34 +00:00
268 lines
9 KiB
TypeScript
268 lines
9 KiB
TypeScript
// ========= 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 }) => (
|
||
<div data-testid="file-preview">{file?.name || 'No file selected'}</div>
|
||
),
|
||
}));
|
||
|
||
// 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: () => <div data-testid="canvas-tab" />,
|
||
}));
|
||
|
||
// xterm needs real layout/canvas APIs; the terminal tab has its own suite.
|
||
vi.mock('@/components/Session/PreviewPanel/tabs/terminal/TerminalTab', () => ({
|
||
TerminalTab: () => <div data-testid="terminal-tab" />,
|
||
}));
|
||
|
||
// 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 <webview>-tag based and driven through the webview registry.
|
||
const openExternal = vi.fn();
|
||
const host = { ipcRenderer: null, electronAPI: { openExternal } };
|
||
|
||
function renderPanel() {
|
||
return render(
|
||
<HostProvider host={host}>
|
||
<PreviewPanel />
|
||
</HostProvider>
|
||
);
|
||
}
|
||
|
||
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', '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 ['Review', '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, 'canvas'));
|
||
const { rerender } = renderPanel();
|
||
expect(screen.getByTestId('canvas-tab')).toBeInTheDocument();
|
||
|
||
act(() =>
|
||
store.choosePreviewTabType(previewSlice().activeTabId!, 'terminal')
|
||
);
|
||
rerender(
|
||
<HostProvider host={host}>
|
||
<PreviewPanel />
|
||
</HostProvider>
|
||
);
|
||
expect(screen.getByTestId('terminal-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,
|
||
});
|
||
});
|
||
});
|