mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-28 18:37:22 +00:00
291 lines
8.3 KiB
TypeScript
291 lines
8.3 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 Session from '@/components/Session';
|
|
import {
|
|
act,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
} from '@testing-library/react';
|
|
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
chatBoxRenderCount: 0,
|
|
pageState: {
|
|
activeWorkspaceTab: 'project',
|
|
setActiveWorkspaceTab: vi.fn(),
|
|
closeSessionPreview: vi.fn(),
|
|
setSessionPreviewProject: vi.fn(),
|
|
sessionPreviewProjectId: 'project-1',
|
|
sessionSidePanelToggleRequestId: 0,
|
|
previewSlice: { open: true, tabs: [], activeTabId: null },
|
|
},
|
|
projectState: {
|
|
activeProjectId: 'project-1',
|
|
getAllChatStores: vi.fn(() => []),
|
|
},
|
|
chatState: {
|
|
activeTaskId: 'task-1',
|
|
tasks: {
|
|
'task-1': {
|
|
status: 'finished',
|
|
messages: [{ id: 'message-1', role: 'user', content: 'hello' }],
|
|
hasMessages: true,
|
|
tokens: 0,
|
|
},
|
|
},
|
|
setSelectedFile: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/components/ChatBox', () => ({
|
|
default: () => {
|
|
mocks.chatBoxRenderCount += 1;
|
|
return <div data-testid="chat-box" />;
|
|
},
|
|
}));
|
|
vi.mock('@/components/Workspace', () => ({
|
|
default: () => <div data-testid="workspace" />,
|
|
}));
|
|
vi.mock('@/components/Session/HeaderBox', () => ({
|
|
HeaderBox: () => <div data-testid="session-header" />,
|
|
}));
|
|
vi.mock('@/components/Session/PreviewPanel', () => ({
|
|
PreviewPanel: () => <div data-testid="preview-panel" />,
|
|
}));
|
|
vi.mock('@/components/Session/SidePanel', () => ({
|
|
SessionSidePanel: () => <div data-testid="session-side-panel-content" />,
|
|
}));
|
|
vi.mock('@/hooks/useChatStoreAdapter', () => ({
|
|
default: () => ({
|
|
chatStore: mocks.chatState,
|
|
projectStore: mocks.projectState,
|
|
}),
|
|
}));
|
|
vi.mock('@/hooks/useProjectEventRuntime', () => ({
|
|
ProjectEventRuntimeProvider: ({ children }: { children: React.ReactNode }) =>
|
|
children,
|
|
}));
|
|
vi.mock('@/lib/sessionMode', () => ({
|
|
inferSessionModeFromTask: () => 'single_agent',
|
|
}));
|
|
vi.mock('@/store/pageTabStore', () => ({
|
|
getSessionPreviewSlice: (state: typeof mocks.pageState) => state.previewSlice,
|
|
usePageTabStore: (selector: (state: typeof mocks.pageState) => unknown) =>
|
|
selector(mocks.pageState),
|
|
WorkspaceTab: { Project: 'project' },
|
|
}));
|
|
vi.mock('@/store/projectRuntimeStore', () => ({
|
|
useProjectRuntimeStore: (
|
|
selector: (state: {
|
|
historyLoadingProjectIds: Record<string, boolean>;
|
|
}) => unknown
|
|
) => selector({ historyLoadingProjectIds: {} }),
|
|
}));
|
|
vi.mock('@/store/spaceStore', () => ({
|
|
useSpaceStore: (
|
|
selector: (state: {
|
|
getProjectMeta: () => { name: string; mode: string; metadata: object };
|
|
updateProjectMeta: ReturnType<typeof vi.fn>;
|
|
}) => unknown
|
|
) =>
|
|
selector({
|
|
getProjectMeta: () => ({
|
|
name: 'Project 1',
|
|
mode: 'single_agent',
|
|
metadata: {},
|
|
}),
|
|
updateProjectMeta: vi.fn(),
|
|
}),
|
|
}));
|
|
vi.mock('framer-motion', () => ({
|
|
AnimatePresence: ({ children }: { children: React.ReactNode }) => children,
|
|
motion: {
|
|
div: ({
|
|
children,
|
|
initial: _initial,
|
|
animate: _animate,
|
|
exit: _exit,
|
|
transition: _transition,
|
|
layout: _layout,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
initial?: unknown;
|
|
animate?: unknown;
|
|
exit?: unknown;
|
|
transition?: unknown;
|
|
layout?: unknown;
|
|
}) => <div {...props}>{children}</div>,
|
|
},
|
|
}));
|
|
|
|
let scheduledFrame: FrameRequestCallback | null = null;
|
|
|
|
class TestPointerEvent extends MouseEvent {
|
|
readonly pointerId: number;
|
|
|
|
constructor(type: string, init: PointerEventInit = {}) {
|
|
super(type, init);
|
|
this.pointerId = init.pointerId ?? 0;
|
|
}
|
|
}
|
|
|
|
function rect(width: number): DOMRect {
|
|
return {
|
|
x: 0,
|
|
y: 0,
|
|
top: 0,
|
|
right: width,
|
|
bottom: 800,
|
|
left: 0,
|
|
width,
|
|
height: 800,
|
|
toJSON: () => ({}),
|
|
};
|
|
}
|
|
|
|
async function renderResizableSession() {
|
|
render(<Session />);
|
|
const separator = screen.getByRole('separator');
|
|
const row = separator.parentElement?.parentElement;
|
|
if (!row) throw new Error('missing Session row');
|
|
|
|
await waitFor(() =>
|
|
expect(row.style.getPropertyValue('--session-chat-width')).toBe('360px')
|
|
);
|
|
|
|
Object.defineProperties(separator, {
|
|
setPointerCapture: { configurable: true, value: vi.fn() },
|
|
hasPointerCapture: {
|
|
configurable: true,
|
|
value: vi.fn(() => true),
|
|
},
|
|
releasePointerCapture: { configurable: true, value: vi.fn() },
|
|
});
|
|
|
|
fireEvent.pointerDown(separator, {
|
|
button: 0,
|
|
buttons: 1,
|
|
clientX: 360,
|
|
isPrimary: true,
|
|
pointerId: 7,
|
|
});
|
|
await waitFor(() =>
|
|
expect(separator).toHaveAttribute('data-resize-handle-state', 'drag')
|
|
);
|
|
|
|
return { row, separator };
|
|
}
|
|
|
|
describe('Session preview resize', () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal('PointerEvent', TestPointerEvent);
|
|
mocks.chatBoxRenderCount = 0;
|
|
mocks.pageState.previewSlice.open = true;
|
|
mocks.pageState.sessionPreviewProjectId = 'project-1';
|
|
mocks.pageState.activeWorkspaceTab = 'project';
|
|
scheduledFrame = null;
|
|
|
|
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(
|
|
function () {
|
|
return rect(this.id === 'session-side-panel' ? 0 : 1200);
|
|
}
|
|
);
|
|
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
|
scheduledFrame = callback;
|
|
return 1;
|
|
});
|
|
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {
|
|
scheduledFrame = null;
|
|
});
|
|
});
|
|
|
|
afterAll(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('coalesces pointer moves and ends a lost pointerup when no button is pressed', async () => {
|
|
const { row, separator } = await renderResizableSession();
|
|
const rendersAfterStart = mocks.chatBoxRenderCount;
|
|
|
|
fireEvent.pointerMove(window, {
|
|
buttons: 1,
|
|
clientX: 430,
|
|
pointerId: 7,
|
|
});
|
|
fireEvent.pointerMove(window, {
|
|
buttons: 1,
|
|
clientX: 500,
|
|
pointerId: 7,
|
|
});
|
|
|
|
expect(window.requestAnimationFrame).toHaveBeenCalledTimes(1);
|
|
expect(mocks.chatBoxRenderCount).toBe(rendersAfterStart);
|
|
expect(screen.getByTestId('chat-box')).toBeInTheDocument();
|
|
expect(
|
|
document.querySelector('[data-preview-resize-shield]')
|
|
).not.toBeNull();
|
|
|
|
act(() => {
|
|
const frame = scheduledFrame;
|
|
scheduledFrame = null;
|
|
frame?.(0);
|
|
});
|
|
expect(row.style.getPropertyValue('--session-chat-width')).toBe('500px');
|
|
|
|
fireEvent.pointerMove(window, {
|
|
buttons: 0,
|
|
clientX: 500,
|
|
pointerId: 7,
|
|
});
|
|
|
|
await waitFor(() =>
|
|
expect(separator).toHaveAttribute('data-resize-handle-state', 'inactive')
|
|
);
|
|
expect(document.querySelector('[data-preview-resize-shield]')).toBeNull();
|
|
expect(document.body.style.cursor).toBe('');
|
|
expect(document.body.style.userSelect).toBe('');
|
|
expect(separator.releasePointerCapture).toHaveBeenCalledWith(7);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
'pointer cancellation',
|
|
(_separator: HTMLElement) =>
|
|
fireEvent.pointerCancel(window, { pointerId: 7 }),
|
|
],
|
|
[
|
|
'capture loss',
|
|
(separator: HTMLElement) =>
|
|
separator.dispatchEvent(new Event('lostpointercapture')),
|
|
],
|
|
[
|
|
'window blur',
|
|
(_separator: HTMLElement) => window.dispatchEvent(new Event('blur')),
|
|
],
|
|
])('ends resize on %s', async (_label, endDrag) => {
|
|
const { separator } = await renderResizableSession();
|
|
|
|
act(() => endDrag(separator));
|
|
|
|
await waitFor(() =>
|
|
expect(separator).toHaveAttribute('data-resize-handle-state', 'inactive')
|
|
);
|
|
expect(document.querySelector('[data-preview-resize-shield]')).toBeNull();
|
|
expect(document.body.style.cursor).toBe('');
|
|
expect(document.body.style.userSelect).toBe('');
|
|
});
|
|
});
|