// ========= 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
; }, })); vi.mock('@/components/Workspace', () => ({ default: () =>
, })); vi.mock('@/components/Session/HeaderBox', () => ({ HeaderBox: () =>
, })); vi.mock('@/components/Session/PreviewPanel', () => ({ PreviewPanel: () =>
, })); vi.mock('@/components/Session/SidePanel', () => ({ SessionSidePanel: () =>
, })); 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; }) => unknown ) => selector({ historyLoadingProjectIds: {} }), })); vi.mock('@/store/spaceStore', () => ({ useSpaceStore: ( selector: (state: { getProjectMeta: () => { name: string; mode: string; metadata: object }; updateProjectMeta: ReturnType; }) => 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 & { initial?: unknown; animate?: unknown; exit?: unknown; transition?: unknown; layout?: unknown; }) =>
{children}
, }, })); 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(); 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(''); }); });