// ========= 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 Workspace from '@/components/Workspace'; import { createSyncedProjectInSpace } from '@/lib/spaceProject'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import type { ComponentProps } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => { const oldSetAttaches = vi.fn(); const newSetAttaches = vi.fn(); const newStartTask = vi.fn().mockResolvedValue(undefined); const newChatState = { activeTaskId: 'new-task', tasks: { 'new-task': { attaches: [], }, }, setHasMessages: vi.fn(), setAttaches: newSetAttaches, startTask: newStartTask, setHasWaitComfirm: vi.fn(), }; const oldChatState = { activeTaskId: 'old-task', tasks: { 'old-task': { attaches: [{ fileName: 'old.txt', filePath: '/old.txt' }], messages: [], hasMessages: false, status: 'pending', taskAssigning: [], }, }, setAttaches: oldSetAttaches, }; const projectState = { activeProjectId: 'old-project', projects: { 'old-project': { id: 'old-project', metadata: {}, mode: 'workforce', }, }, navLeadByProjectId: {}, isEmptyProject: vi.fn(() => false), setActiveProject: vi.fn(), getActiveChatStore: vi.fn(() => ({ getState: () => newChatState, })), }; const spaceState = { activeSpaceId: 'space-1', spaces: { 'space-1': { id: 'space-1', sourceType: 'blank', status: 'active', }, }, projectsBySpaceId: {}, getProjectMeta: vi.fn(() => null), setActiveSpace: vi.fn(), }; const pageState = { activeWorkspaceTab: 'workforce', workspaceChatFocusRequestId: 0, customAgentFolderPathByProjectId: {}, setActiveWorkspaceTab: vi.fn(), }; return { newChatState, newStartTask, newSetAttaches, oldChatState, oldSetAttaches, pageState, projectState, spaceState, }; }); vi.mock('@/hooks/useChatStoreAdapter', () => ({ default: () => ({ chatStore: mocks.oldChatState, projectStore: mocks.projectState, }), })); vi.mock('@/hooks/useModelConfigCheck', () => ({ useModelConfigCheck: () => ({ hasModel: true }), })); vi.mock('@/host', () => ({ useHost: () => ({ electronAPI: {} }), })); vi.mock('@/store/authStore', () => ({ getAuthStore: () => ({ language: 'en', setLanguage: vi.fn(), }), useAuthStore: () => ({ modelType: 'local', setWorkerList: vi.fn(), }), useWorkerList: () => [], })); vi.mock('@/store/pageTabStore', () => { const usePageTabStore = Object.assign( (selector: (state: typeof mocks.pageState) => unknown) => selector(mocks.pageState), { getState: () => mocks.pageState } ); return { usePageTabStore }; }); vi.mock('@/store/projectRuntimeStore', () => { const useProjectRuntimeStore = Object.assign( (selector: (state: typeof mocks.projectState) => unknown) => selector(mocks.projectState), { getState: () => mocks.projectState } ); return { useProjectRuntimeStore }; }); vi.mock('@/store/spaceStore', async (importOriginal) => { const actual = await importOriginal(); const useSpaceStore = Object.assign( (selector: (state: typeof mocks.spaceState) => unknown) => selector(mocks.spaceState), { getState: () => mocks.spaceState } ); return { ...actual, useSpaceStore, }; }); vi.mock('@/lib/spaceProject', () => ({ createSyncedProjectInSpace: vi.fn(), })); vi.mock('@/components/ChatBox/BottomBox', () => ({ default: ({ inputProps }: { inputProps: any }) => (
inputProps.onChange(event.target.value)} />
), })); vi.mock('@/components/AddWorker', () => ({ AddWorker: () => null, })); vi.mock('@/components/Workspace/SingleAgentList', () => ({ SingleAgentList: () => null, })); vi.mock('@/components/Workspace/WorkforceAgentList', () => ({ WorkforceAgentList: () => null, })); vi.mock('@/components/Workspace/WorkspaceProjectPicker', () => ({ WorkspaceProjectPicker: () =>
Space switch
, })); const renderWorkspace = (props: ComponentProps = {}) => render( ); describe('Workspace', () => { beforeEach(() => { vi.clearAllMocks(); mocks.spaceState.projectsBySpaceId = {}; vi.mocked(createSyncedProjectInSpace).mockResolvedValue({ projectId: 'new-project', spaceId: 'space-1', }); mocks.newStartTask.mockResolvedValue(undefined); }); it('creates a fresh project and sends only Workspace draft attachments', async () => { renderWorkspace(); fireEvent.change(screen.getByLabelText('workspace-message'), { target: { value: 'Start fresh work' }, }); fireEvent.click(screen.getByText('Attach draft')); fireEvent.click(screen.getByText('Send')); await waitFor(() => { expect(createSyncedProjectInSpace).toHaveBeenCalledTimes(1); }); expect(createSyncedProjectInSpace).toHaveBeenCalledWith( expect.objectContaining({ spaceId: 'space-1', name: 'Start fresh work', }) ); expect(mocks.newStartTask).toHaveBeenCalledWith( 'new-task', undefined, undefined, undefined, 'Start fresh work', [{ fileName: 'draft.txt', filePath: '/draft.txt' }], undefined, 'new-project', 'single-agent' ); expect(mocks.oldSetAttaches).not.toHaveBeenCalled(); }); it('does not show a Workspace Profile control in the header', () => { renderWorkspace(); expect( screen.queryByRole('button', { name: 'layout.workspace-profile' }) ).not.toBeInTheDocument(); }); it('centers the composer without Workspace management subpages', () => { renderWorkspace(); expect( screen.queryByRole('complementary', { name: 'Workspace management' }) ).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: 'Space settings' }) ).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: 'Memory settings' }) ).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: /All projects/ }) ).not.toBeInTheDocument(); expect(screen.getByLabelText('Workspace header')).toHaveClass( 'flex-1', 'items-center' ); }); it('uses one left-aligned Cowork row without the Space switch above BottomBox', () => { const { container } = renderWorkspace(); expect(screen.queryByText('Space switch')).not.toBeInTheDocument(); const coworkLabel = screen.getByText('Cowork with'); const singleAgentLabel = screen.getByText('Single Agent'); const coworkRow = coworkLabel.closest('[data-workspace-cowork-row]'); const agentList = container.querySelector('[data-workspace-agent-list]'); const bottomBox = container.querySelector('[data-workspace-bottom-box]'); const inputSection = container.querySelector( '[data-workspace-input-section]' ); const workspaceHeader = screen.getByLabelText('Workspace header'); expect(coworkLabel).toHaveClass('text-heading-lg', 'font-display'); expect(singleAgentLabel).toHaveClass('text-heading-lg', 'font-display'); expect(workspaceHeader).toHaveClass('flex-1', 'items-center', 'gap-0'); expect(inputSection).toHaveClass('items-center', 'p-4'); expect(coworkRow).toHaveClass( 'min-h-[46px]', 'items-center', 'justify-start' ); expect(agentList).toHaveClass( 'h-[46px]', 'min-h-[46px]', 'items-center', 'justify-start', 'overflow-visible' ); expect(coworkRow?.nextElementSibling).toBe(bottomBox); }); it('uses the same Cowork composer for the new-project variant', () => { const { container } = renderWorkspace({ variant: 'new-project', embedded: true, }); expect(screen.getByText('Cowork with')).toBeInTheDocument(); expect(screen.getByText('Single Agent')).toBeInTheDocument(); expect(screen.queryByText('Space switch')).not.toBeInTheDocument(); expect(container.querySelector('#workspace-bottom-group')).toBeNull(); expect(screen.getByLabelText('Workspace header')).toHaveClass( 'flex-1', 'items-center', 'gap-0' ); }); it('keeps the agent-list height fixed across Single Agent and Workforce modes', () => { const singleAgentView = renderWorkspace({ sessionMode: 'single-agent' }); const singleAgentList = singleAgentView.container.querySelector( '[data-workspace-agent-list]' ); expect(singleAgentList).toHaveClass('h-[46px]', 'min-h-[46px]'); singleAgentView.unmount(); const workforceView = renderWorkspace({ sessionMode: 'workforce' }); const workforceAgentList = workforceView.container.querySelector( '[data-workspace-agent-list]' ); expect(workforceAgentList).toHaveClass('h-[46px]', 'min-h-[46px]'); }); it('shows the mode label only for Single Agent mode', () => { const { unmount } = renderWorkspace({ sessionMode: 'single-agent' }); expect(screen.getByText('Single Agent')).toBeInTheDocument(); unmount(); renderWorkspace({ sessionMode: 'workforce' }); expect(screen.queryByText('Single Agent')).not.toBeInTheDocument(); }); it('guards against duplicate submissions while project creation is pending', async () => { let resolveCreation: ((value: { projectId: string; spaceId: string }) => void) | undefined; vi.mocked(createSyncedProjectInSpace).mockImplementation( () => new Promise((resolve) => { resolveCreation = resolve; }) ); renderWorkspace(); fireEvent.change(screen.getByLabelText('workspace-message'), { target: { value: 'Only once' }, }); fireEvent.click(screen.getByText('Send')); fireEvent.click(screen.getByText('Send')); expect(createSyncedProjectInSpace).toHaveBeenCalledTimes(1); resolveCreation?.({ projectId: 'new-project', spaceId: 'space-1' }); await waitFor(() => expect(mocks.newStartTask).toHaveBeenCalledTimes(1)); }); });