mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-28 02:11:38 +00:00
978 lines
31 KiB
TypeScript
978 lines
31 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 SettingsSection from '@/components/Settings/SettingsSection';
|
|
import SettingsPage from '@/pages/Settings';
|
|
import { useSettingsStore } from '@/store/settingsStore';
|
|
import { useSpaceStore } from '@/store/spaceStore';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import {
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const homeOverviewMocks = vi.hoisted(() => ({
|
|
fetchConnectedProviders: vi.fn(),
|
|
listMemoryEntries: vi.fn(),
|
|
}));
|
|
|
|
const pageMotionMocks = vi.hoisted(() => ({
|
|
reduced: false,
|
|
}));
|
|
|
|
const platformMocks = vi.hoisted(() => ({
|
|
desktop: true,
|
|
}));
|
|
|
|
vi.mock('@/client/platform', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('@/client/platform')>()),
|
|
isDesktop: () => platformMocks.desktop,
|
|
}));
|
|
|
|
vi.mock('framer-motion', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('framer-motion')>()),
|
|
useReducedMotion: () => pageMotionMocks.reduced,
|
|
}));
|
|
|
|
vi.mock('@/api/connectors', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('@/api/connectors')>()),
|
|
fetchConnectedProviders: homeOverviewMocks.fetchConnectedProviders,
|
|
}));
|
|
|
|
vi.mock('@/service/memoryApi', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('@/service/memoryApi')>()),
|
|
listMemoryEntries: homeOverviewMocks.listMemoryEntries,
|
|
}));
|
|
|
|
vi.mock('@/hooks/queries/useTriggerQueries', () => ({
|
|
useUserTriggerCountQuery: () => ({ data: 0 }),
|
|
}));
|
|
|
|
vi.mock('@/service/historyApi', () => ({
|
|
fetchGroupedHistoryTasks: vi.fn(() => new Promise(() => undefined)),
|
|
}));
|
|
|
|
vi.mock('@/service/triggerApi', () => ({
|
|
proxyFetchTriggers: vi.fn(() => new Promise(() => undefined)),
|
|
}));
|
|
|
|
vi.mock('@/components/Settings/General', () => ({
|
|
default: () => <div data-testid="general-settings" />,
|
|
}));
|
|
|
|
vi.mock('@/components/Settings/Appearance', () => ({
|
|
default: () => <div data-testid="appearance-settings" />,
|
|
}));
|
|
|
|
vi.mock('@/components/Settings/Privacy', () => ({
|
|
default: () => <div data-testid="privacy-settings" />,
|
|
}));
|
|
|
|
vi.mock('@/store/authStore', () => {
|
|
const authState = {
|
|
appearance: 'light',
|
|
language: 'en',
|
|
token: 'token',
|
|
username: 'Douglas',
|
|
email: 'douglas@example.com',
|
|
user_id: 7,
|
|
};
|
|
const useAuthStore = (
|
|
selector: (state: typeof authState) => unknown = (state) => state
|
|
) => selector(authState);
|
|
|
|
return {
|
|
getAuthStore: () => authState,
|
|
useAuthStore,
|
|
useWorkerList: () => [],
|
|
};
|
|
});
|
|
|
|
function renderSettingsPage(
|
|
initialEntry = '/home?section=settings&tab=models'
|
|
) {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
return render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<MemoryRouter initialEntries={[initialEntry]}>
|
|
<SettingsPage />
|
|
</MemoryRouter>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
function getSettingsHeader() {
|
|
const header = document.querySelector('header');
|
|
expect(header).not.toBeNull();
|
|
return header as HTMLElement;
|
|
}
|
|
|
|
describe('SettingsPage', () => {
|
|
beforeEach(() => {
|
|
pageMotionMocks.reduced = false;
|
|
platformMocks.desktop = true;
|
|
useSettingsStore.setState({
|
|
activeSection: 'models',
|
|
});
|
|
homeOverviewMocks.fetchConnectedProviders.mockResolvedValue([
|
|
{ service: 'github' },
|
|
{ service: 'notion' },
|
|
]);
|
|
homeOverviewMocks.listMemoryEntries.mockResolvedValue({
|
|
scope_state: {
|
|
token_limit: 5000,
|
|
current_token_count: 1000,
|
|
},
|
|
items: [],
|
|
});
|
|
});
|
|
|
|
it('hides Desktop-only Agent Plugin import from the web dialog', async () => {
|
|
platformMocks.desktop = false;
|
|
const user = userEvent.setup();
|
|
renderSettingsPage('/home?section=spaces');
|
|
|
|
await user.click(await screen.findByRole('button', { name: 'New Space' }));
|
|
await user.click(
|
|
screen.getByRole('button', { name: 'Import from Workspace Bundle' })
|
|
);
|
|
|
|
const bundleOptions = screen.getByRole('group', {
|
|
name: 'Bundle import options',
|
|
});
|
|
expect(bundleOptions).toHaveClass('grid-cols-1');
|
|
expect(
|
|
within(bundleOptions).queryByRole('button', {
|
|
name: 'Import Agent Plugin as Bundle',
|
|
})
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('renders scoped navigation in the shared app shell', () => {
|
|
renderSettingsPage();
|
|
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
const main = screen.getByRole('main');
|
|
const sidebar = screen.getByRole('complementary', {
|
|
name: 'Home',
|
|
});
|
|
const contentShell = document.querySelector('.scrollbar-always-visible');
|
|
expect(contentShell).toHaveClass(
|
|
'overflow-y-scroll',
|
|
'[scrollbar-gutter:stable]'
|
|
);
|
|
expect(contentShell?.firstElementChild).toHaveClass('px-8');
|
|
expect(
|
|
within(sidebar).getByRole('navigation', { name: 'Home' })
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.queryByRole('button', { name: 'Workspace Bundle' })
|
|
).not.toBeInTheDocument();
|
|
const selectedTab = screen.getByRole('button', { name: 'Models' });
|
|
const header = getSettingsHeader();
|
|
const heading = within(header).getByRole('heading', {
|
|
name: 'Models',
|
|
level: 1,
|
|
});
|
|
expect(main).toContainElement(header);
|
|
expect(heading).toHaveFocus();
|
|
expect(header).toHaveClass('h-[44px]');
|
|
expect(within(header).getByText('Models')).toHaveClass(
|
|
'text-body-md',
|
|
'font-bold'
|
|
);
|
|
expect(
|
|
within(header).queryByRole('button', {
|
|
name: 'Back',
|
|
})
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
screen.queryByRole('heading', {
|
|
name: 'layout.workspace-active-scope',
|
|
level: 3,
|
|
})
|
|
).not.toBeInTheDocument();
|
|
expect(selectedTab).toHaveAttribute('aria-current', 'page');
|
|
expect(selectedTab).toHaveClass(
|
|
'bg-ds-bg-neutral-subtle-default',
|
|
'h-8',
|
|
'w-full'
|
|
);
|
|
const homeLabel = within(sidebar).getByText('Home');
|
|
const globalSettingLabel = within(sidebar).getByText('Global Settings');
|
|
expect(
|
|
homeLabel.compareDocumentPosition(globalSettingLabel) &
|
|
Node.DOCUMENT_POSITION_FOLLOWING
|
|
).toBeTruthy();
|
|
expect(
|
|
within(sidebar).getByRole('button', { name: 'Spaces' })
|
|
).toBeInTheDocument();
|
|
expect(
|
|
within(sidebar).queryByRole('button', { name: 'Projects' })
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
within(sidebar).queryByRole('button', { name: 'Tasks' })
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
within(sidebar).queryByRole('button', { name: 'Automations' })
|
|
).not.toBeInTheDocument();
|
|
expect(globalSettingLabel).toBeInTheDocument();
|
|
expect(
|
|
globalSettingLabel.compareDocumentPosition(selectedTab) &
|
|
Node.DOCUMENT_POSITION_FOLLOWING
|
|
).toBeTruthy();
|
|
expect(screen.getByText('Browser')).toBeInTheDocument();
|
|
expect(screen.getByText('Extension')).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole('button', { name: 'Models' }).querySelector('svg')
|
|
).toHaveClass('lucide-sparkles');
|
|
expect(
|
|
screen.getByRole('button', { name: 'Sub Agents' }).querySelector('svg')
|
|
).toHaveClass('lucide-bot');
|
|
expect(
|
|
screen.getByRole('button', { name: 'Skills' }).querySelector('svg')
|
|
).toHaveClass('lucide-wand-sparkles');
|
|
expect(
|
|
screen.getByRole('button', { name: 'Browser' }).querySelector('svg')
|
|
).toHaveClass('lucide-globe');
|
|
expect(
|
|
screen.getByRole('button', { name: 'Extension' }).querySelector('svg')
|
|
).toHaveClass('lucide-puzzle');
|
|
expect(
|
|
within(sidebar)
|
|
.getByRole('button', { name: 'Settings' })
|
|
.querySelector('svg')
|
|
).toHaveClass('lucide-settings');
|
|
expect(
|
|
within(sidebar).queryByRole('button', { name: 'General' })
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
within(sidebar).queryByRole('button', { name: 'Appearance' })
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
within(sidebar).queryByRole('button', { name: 'Privacy' })
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('switches between Home and Settings sections in the same shell', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
renderSettingsPage();
|
|
|
|
const spacesTab = screen.getByRole('button', { name: 'Spaces' });
|
|
const modelsTab = screen.getByRole('button', { name: 'Models' });
|
|
await user.click(spacesTab);
|
|
|
|
await waitFor(() => {
|
|
expect(spacesTab).toHaveAttribute('aria-current', 'page');
|
|
expect(modelsTab).not.toHaveAttribute('aria-current');
|
|
});
|
|
|
|
const overview = document.querySelector(
|
|
'[data-home-spaces-overview]'
|
|
) as HTMLElement;
|
|
const toolbar = document.querySelector(
|
|
'[data-home-spaces-toolbar]'
|
|
) as HTMLElement;
|
|
const list = document.querySelector('[data-home-spaces-list]');
|
|
expect(toolbar).toHaveClass(
|
|
'sticky',
|
|
'-top-px',
|
|
'z-20',
|
|
'bg-ds-bg-neutral-subtle-default'
|
|
);
|
|
expect(document.querySelector('main > header')).not.toBeInTheDocument();
|
|
expect(overview).toHaveTextContent(/Morning|Good Afternoon|Evening/);
|
|
expect(overview).toHaveTextContent('Douglas');
|
|
expect(within(overview).queryByText('Status')).not.toBeInTheDocument();
|
|
expect(within(overview).queryByText('Tasks')).not.toBeInTheDocument();
|
|
expect(
|
|
within(overview)
|
|
.getByText('Spaces')
|
|
.parentElement?.parentElement?.querySelector('svg')
|
|
).toHaveClass('lucide-folder');
|
|
expect(
|
|
within(overview)
|
|
.getByText('Connectors')
|
|
.parentElement?.parentElement?.querySelector('svg')
|
|
).toHaveClass('lucide-cable');
|
|
expect(await within(overview).findByText('2')).toBeInTheDocument();
|
|
expect(
|
|
within(overview)
|
|
.getByText('Skills')
|
|
.parentElement?.parentElement?.querySelector('svg')
|
|
).toHaveClass('lucide-wand-sparkles');
|
|
expect(
|
|
within(overview)
|
|
.getByText('Memory left')
|
|
.parentElement?.parentElement?.querySelector('svg')
|
|
).toHaveClass('lucide-brain');
|
|
expect(await within(overview).findByText('4,000')).toBeInTheDocument();
|
|
expect(
|
|
within(toolbar).getByPlaceholderText('Search spaces...')
|
|
).toBeInTheDocument();
|
|
expect(
|
|
within(toolbar).getByRole('tab', { name: 'List' })
|
|
).toBeInTheDocument();
|
|
expect(
|
|
within(toolbar).queryByRole('tab', { name: 'Board' })
|
|
).not.toBeInTheDocument();
|
|
const newSpaceButton = within(toolbar).getByRole('button', {
|
|
name: 'New Space',
|
|
});
|
|
expect(newSpaceButton.querySelector('svg')).not.toBeInTheDocument();
|
|
expect(
|
|
toolbar.compareDocumentPosition(list!) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
).toBeTruthy();
|
|
|
|
await user.click(newSpaceButton);
|
|
const newSpaceDialog = await screen.findByRole('dialog', {
|
|
name: 'Create a new Space',
|
|
});
|
|
const newSpaceOptions = within(newSpaceDialog).getByRole('group', {
|
|
name: 'New Space options',
|
|
});
|
|
expect(newSpaceOptions).toHaveClass('grid-cols-3');
|
|
expect(
|
|
within(newSpaceOptions).getByRole('button', {
|
|
name: 'Start from scratch',
|
|
})
|
|
).toBeInTheDocument();
|
|
expect(
|
|
within(newSpaceOptions).getByRole('button', {
|
|
name: 'Use a local folder',
|
|
})
|
|
).toBeInTheDocument();
|
|
await user.click(
|
|
within(newSpaceOptions).getByRole('button', {
|
|
name: 'Import from Workspace Bundle',
|
|
})
|
|
);
|
|
|
|
const bundleOptionsDialog = await screen.findByRole('dialog', {
|
|
name: 'Import a Bundle',
|
|
});
|
|
const bundleOptions = within(bundleOptionsDialog).getByRole('group', {
|
|
name: 'Bundle import options',
|
|
});
|
|
expect(bundleOptions).toHaveClass('grid-cols-2');
|
|
expect(
|
|
within(bundleOptions).getByRole('button', {
|
|
name: 'Import Agent Plugin as Bundle',
|
|
})
|
|
).toBeInTheDocument();
|
|
await user.click(
|
|
within(bundleOptions).getByRole('button', {
|
|
name: 'Add Workspace Bundle name',
|
|
})
|
|
);
|
|
|
|
const workspaceBundleDialog = await screen.findByRole('dialog', {
|
|
name: 'Import Workspace Bundle',
|
|
});
|
|
expect(
|
|
await within(workspaceBundleDialog).findByRole('textbox', {
|
|
name: 'Workspace Bundle share handle',
|
|
})
|
|
).toBeInTheDocument();
|
|
await user.click(
|
|
within(workspaceBundleDialog).getByRole('button', { name: 'Close' })
|
|
);
|
|
|
|
await user.click(newSpaceButton);
|
|
const reopenedNewSpaceDialog = await screen.findByRole('dialog', {
|
|
name: 'Create a new Space',
|
|
});
|
|
await user.click(
|
|
within(reopenedNewSpaceDialog).getByRole('button', {
|
|
name: 'Import from Workspace Bundle',
|
|
})
|
|
);
|
|
const reopenedBundleOptions = await screen.findByRole('dialog', {
|
|
name: 'Import a Bundle',
|
|
});
|
|
await user.click(
|
|
within(reopenedBundleOptions).getByRole('button', {
|
|
name: 'Import Agent Plugin as Bundle',
|
|
})
|
|
);
|
|
const agentPluginDialog = await screen.findByRole('dialog', {
|
|
name: 'Import Agent Plugin as Bundle',
|
|
});
|
|
expect(
|
|
await within(agentPluginDialog).findByRole('button', {
|
|
name: 'Select directory or archive',
|
|
})
|
|
).toBeInTheDocument();
|
|
await user.click(
|
|
within(agentPluginDialog).getByRole('button', { name: 'Close' })
|
|
);
|
|
|
|
await user.click(modelsTab);
|
|
|
|
await waitFor(() => {
|
|
expect(modelsTab).toHaveAttribute('aria-current', 'page');
|
|
expect(spacesTab).not.toHaveAttribute('aria-current');
|
|
});
|
|
});
|
|
|
|
it('combines the app settings categories into one vertical page', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
renderSettingsPage();
|
|
|
|
const sidebar = screen.getByRole('complementary', { name: 'Home' });
|
|
await user.click(within(sidebar).getByRole('button', { name: 'Settings' }));
|
|
|
|
const general = await screen.findByTestId('general-settings');
|
|
const appearance = screen.getByTestId('appearance-settings');
|
|
const privacy = screen.getByTestId('privacy-settings');
|
|
const about = screen.getByRole('img', { name: 'Eigent' });
|
|
|
|
expect(
|
|
general.compareDocumentPosition(appearance) &
|
|
Node.DOCUMENT_POSITION_FOLLOWING
|
|
).toBeTruthy();
|
|
expect(
|
|
appearance.compareDocumentPosition(privacy) &
|
|
Node.DOCUMENT_POSITION_FOLLOWING
|
|
).toBeTruthy();
|
|
expect(
|
|
privacy.compareDocumentPosition(about) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
).toBeTruthy();
|
|
expect(
|
|
screen.queryByRole('heading', {
|
|
name: /general|appearance|privacy|about/i,
|
|
})
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('updates the content header for a settings sub-tab', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
renderSettingsPage();
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Skills' }));
|
|
const exampleSkillsTab = await screen.findByRole('tab', {
|
|
name: 'Example skills',
|
|
});
|
|
const yourSkillsTab = screen.getByRole('tab', {
|
|
name: 'Your skills',
|
|
});
|
|
const header = getSettingsHeader();
|
|
const heading = await within(header).findByRole('heading', {
|
|
name: 'Your skills',
|
|
level: 1,
|
|
});
|
|
expect(heading).toHaveClass('sr-only');
|
|
expect(header).toContainElement(exampleSkillsTab);
|
|
expect(within(header).getByRole('tablist')).toHaveClass(
|
|
'rounded-xl',
|
|
'bg-ds-bg-neutral-strong-default',
|
|
'ring-1'
|
|
);
|
|
expect(exampleSkillsTab).toHaveAttribute('data-tabs-appearance', 'default');
|
|
expect(exampleSkillsTab).toHaveClass(
|
|
'rounded-xl',
|
|
'bg-ds-bg-neutral-strong-default'
|
|
);
|
|
expect(
|
|
within(header).getByRole('button', { name: 'Search' })
|
|
).toBeInTheDocument();
|
|
expect(
|
|
within(header).getByRole('button', { name: 'Add Skill' })
|
|
).toBeInTheDocument();
|
|
|
|
expect(yourSkillsTab).toHaveAttribute('aria-selected', 'true');
|
|
expect(
|
|
within(header).getByText('Your skills', { selector: 'span' })
|
|
).toBeInTheDocument();
|
|
|
|
await user.click(exampleSkillsTab);
|
|
|
|
await waitFor(() => {
|
|
expect(exampleSkillsTab).toHaveAttribute('aria-selected', 'true');
|
|
});
|
|
expect(
|
|
screen.queryByRole('heading', {
|
|
name: 'Example skills',
|
|
level: 3,
|
|
})
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('clears section-owned header controls during a page switch', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
renderSettingsPage();
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Skills' }));
|
|
const header = getSettingsHeader();
|
|
|
|
expect(await within(header).findByRole('tablist')).toBeInTheDocument();
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Channels' }));
|
|
|
|
await waitFor(() => {
|
|
expect(within(header).queryByRole('tablist')).not.toBeInTheDocument();
|
|
expect(within(header).getByText('Channels')).toBeInTheDocument();
|
|
});
|
|
await waitFor(() => {
|
|
expect(
|
|
document.querySelector('[data-settings-section="channels"]')
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('switches to the Space detail layout without changing the shared shell', async () => {
|
|
const user = userEvent.setup();
|
|
const now = Date.now();
|
|
useSpaceStore.setState((state) => ({
|
|
...state,
|
|
spaces: {
|
|
...state.spaces,
|
|
'space-1': {
|
|
id: 'space-1',
|
|
name: 'Design Space',
|
|
description: 'Product design work',
|
|
sourceType: 'folder',
|
|
rootPath: '/work/design-space',
|
|
status: 'active',
|
|
schemaVersion: 1,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
'legacy-untitled': {
|
|
id: 'legacy-untitled',
|
|
name: 'Untitled Space',
|
|
sourceType: 'blank',
|
|
status: 'active',
|
|
schemaVersion: 1,
|
|
createdAt: now - 1,
|
|
updatedAt: now - 1,
|
|
},
|
|
},
|
|
projectsBySpaceId: {
|
|
...state.projectsBySpaceId,
|
|
'space-1': {},
|
|
'legacy-untitled': {},
|
|
},
|
|
projectsSyncedAt: {
|
|
...state.projectsSyncedAt,
|
|
'space-1': now,
|
|
},
|
|
}));
|
|
|
|
renderSettingsPage('/home?section=spaces');
|
|
|
|
const homeSpaceCard = (await screen.findByText('Design Space')).closest(
|
|
'[role="button"]'
|
|
) as HTMLElement;
|
|
expect(homeSpaceCard).toBeInTheDocument();
|
|
await user.click(homeSpaceCard);
|
|
|
|
const detailSidebar = await screen.findByRole('complementary', {
|
|
name: 'Spaces',
|
|
});
|
|
expect(
|
|
document.querySelector('[data-home-space-sidebar-pane="detail"]')
|
|
).toHaveAttribute('data-space-navigation-direction', 'forward');
|
|
expect(
|
|
document.querySelector('[data-home-space-sidebar-pane="detail"]')
|
|
).toHaveAttribute('data-space-navigation-motion', 'full');
|
|
expect(
|
|
document.querySelector('[data-home-space-content-pane="detail"]')
|
|
).toHaveAttribute('data-space-navigation-motion', 'full');
|
|
expect(
|
|
within(detailSidebar).getByRole('button', { name: 'Design Space' })
|
|
).toHaveAttribute('aria-current', 'page');
|
|
expect(
|
|
within(detailSidebar).getByRole('button', { name: 'Back to Home' })
|
|
).toBeInTheDocument();
|
|
const newSpaceTab = within(detailSidebar).getByRole('button', {
|
|
name: 'New Space',
|
|
});
|
|
expect(newSpaceTab.querySelector('svg')).toHaveClass('lucide-plus');
|
|
expect(
|
|
newSpaceTab.compareDocumentPosition(
|
|
within(detailSidebar).getByText('Spaces')
|
|
) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
).toBeTruthy();
|
|
expect(
|
|
within(detailSidebar).queryByRole('button', { name: 'Untitled Space' })
|
|
).not.toBeInTheDocument();
|
|
await user.click(newSpaceTab);
|
|
expect(
|
|
await screen.findByRole('dialog', { name: 'Create a new Space' })
|
|
).toBeInTheDocument();
|
|
await user.click(screen.getByRole('button', { name: 'Close' }));
|
|
expect(
|
|
detailSidebar.querySelector('.lucide-check')
|
|
).not.toBeInTheDocument();
|
|
const designSpaceTab = within(detailSidebar).getByRole('button', {
|
|
name: 'Design Space',
|
|
});
|
|
const spaceMoreButton = within(detailSidebar).getByRole('button', {
|
|
name: 'More actions: Design Space',
|
|
});
|
|
expect(spaceMoreButton.closest('.group')).toContainElement(designSpaceTab);
|
|
|
|
await user.click(spaceMoreButton);
|
|
await user.click(
|
|
within(screen.getByRole('menu')).getByRole('menuitem', {
|
|
name: 'Rename Space',
|
|
})
|
|
);
|
|
const renameDialog = screen.getByRole('alertdialog', {
|
|
name: 'Rename Space',
|
|
});
|
|
expect(within(renameDialog).getByPlaceholderText('Space name')).toHaveValue(
|
|
'Design Space'
|
|
);
|
|
await user.click(
|
|
within(renameDialog).getByRole('button', { name: 'Cancel' })
|
|
);
|
|
|
|
await user.click(spaceMoreButton);
|
|
await user.click(
|
|
within(screen.getByRole('menu')).getByRole('menuitem', {
|
|
name: 'Delete',
|
|
})
|
|
);
|
|
const deleteDialog = screen.getByRole('alertdialog', { name: 'Delete' });
|
|
expect(deleteDialog).toHaveTextContent(
|
|
'Are you sure you want to delete this Space and all its Projects?'
|
|
);
|
|
await user.click(
|
|
within(deleteDialog).getByRole('button', { name: 'Cancel' })
|
|
);
|
|
const detailHeader = document.querySelector('main header');
|
|
expect(detailHeader).not.toBeInTheDocument();
|
|
expect(
|
|
within(screen.getByRole('main')).getByText('Design Space')
|
|
).toHaveClass('!text-body-lg');
|
|
expect(screen.getByText('Product design work')).toBeInTheDocument();
|
|
expect(screen.getByText('Local')).toBeInTheDocument();
|
|
|
|
for (const tabName of [
|
|
'Projects',
|
|
'Tasks',
|
|
'Automations',
|
|
'Context',
|
|
'Memory',
|
|
'Space Settings',
|
|
]) {
|
|
expect(screen.getByRole('tab', { name: tabName })).toBeInTheDocument();
|
|
}
|
|
expect(screen.getByRole('tablist', { name: 'Space content' })).toHaveClass(
|
|
'gap-2',
|
|
'pb-2'
|
|
);
|
|
const spaceTabRow = screen.getByRole('tablist', {
|
|
name: 'Space content',
|
|
}).parentElement;
|
|
expect(spaceTabRow).toHaveClass('justify-between');
|
|
const openWorkspaceButton = within(spaceTabRow as HTMLElement).getByRole(
|
|
'button',
|
|
{
|
|
name: 'Open Workspace',
|
|
}
|
|
);
|
|
expect(openWorkspaceButton).toHaveAttribute('data-variant', 'primary');
|
|
expect(openWorkspaceButton).toHaveClass('!rounded-full');
|
|
expect(
|
|
within(screen.getByRole('tab', { name: 'Projects' })).getByText(
|
|
'Projects'
|
|
)
|
|
).toHaveClass('!text-body-sm', 'font-bold');
|
|
expect(screen.getByRole('tab', { name: 'Projects' })).toHaveAttribute(
|
|
'aria-selected',
|
|
'true'
|
|
);
|
|
expect(
|
|
screen.getByRole('tab', { name: 'Context' }).querySelector('svg')
|
|
).toHaveClass('lucide-library');
|
|
expect(
|
|
screen.getByRole('tab', { name: 'Space Settings' }).querySelector('svg')
|
|
).toHaveClass('lucide-settings');
|
|
fireEvent.pointerEnter(screen.getByRole('tab', { name: 'Memory' }), {
|
|
pointerType: 'mouse',
|
|
});
|
|
expect(document.querySelector('[data-space-detail-tab-hover]')).toHaveClass(
|
|
'rounded-full',
|
|
'bg-ds-bg-neutral-default-default'
|
|
);
|
|
expect(screen.getByRole('tab', { name: 'Memory' })).toHaveClass(
|
|
'rounded-full'
|
|
);
|
|
const stickyTabs = document.querySelector('[data-space-tabs-sticky]');
|
|
expect(stickyTabs).toHaveClass(
|
|
'sticky',
|
|
'-top-px',
|
|
'border-b-1',
|
|
'bg-ds-bg-neutral-subtle-default'
|
|
);
|
|
const detailRails = [
|
|
document.querySelector('[data-space-detail-summary-rail]'),
|
|
document.querySelector('[data-space-detail-tabs-rail]'),
|
|
document.querySelector('[data-space-detail-content-rail]'),
|
|
];
|
|
for (const rail of detailRails) {
|
|
expect(rail).toHaveClass('mx-auto', 'w-full', 'max-w-[1100px]');
|
|
}
|
|
expect(detailRails[2]?.parentElement).toHaveClass('px-8');
|
|
expect(detailRails[2]).not.toHaveClass('px-8');
|
|
expect(document.querySelector('[data-space-stat="Status"]')).toHaveClass(
|
|
'items-center'
|
|
);
|
|
|
|
await user.click(screen.getByRole('tab', { name: 'Tasks' }));
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('tab', { name: 'Tasks' })).toHaveAttribute(
|
|
'aria-selected',
|
|
'true'
|
|
);
|
|
});
|
|
expect(screen.getByRole('tab', { name: 'Projects' })).toHaveAttribute(
|
|
'aria-selected',
|
|
'false'
|
|
);
|
|
|
|
await user.click(
|
|
within(detailSidebar).getByRole('button', { name: 'Back to Home' })
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole('complementary', { name: 'Home' })
|
|
).toBeInTheDocument();
|
|
});
|
|
expect(
|
|
document.querySelector('[data-home-space-sidebar-pane="home"]')
|
|
).toHaveAttribute('data-space-navigation-direction', 'back');
|
|
expect(
|
|
document.querySelector('[data-home-space-sidebar-pane="home"]')
|
|
).toHaveAttribute('data-space-navigation-motion', 'full');
|
|
expect(
|
|
document.querySelector('[data-home-space-content-pane="home"]')
|
|
).toHaveAttribute('data-space-navigation-motion', 'full');
|
|
expect(screen.getByRole('button', { name: 'Models' })).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'Spaces' })).toHaveAttribute(
|
|
'aria-current',
|
|
'page'
|
|
);
|
|
|
|
const cardWorkspaceButtons = await waitFor(() => {
|
|
const buttons = Array.from(
|
|
document.querySelectorAll<HTMLButtonElement>(
|
|
'[data-home-space-open-workspace][data-layout="card"]'
|
|
)
|
|
);
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
return buttons;
|
|
});
|
|
expect(cardWorkspaceButtons[0]).toHaveAttribute('data-variant', 'ghost');
|
|
expect(cardWorkspaceButtons[0]).toHaveClass(
|
|
'cursor-pointer',
|
|
'rounded-lg',
|
|
'font-medium'
|
|
);
|
|
expect(
|
|
cardWorkspaceButtons[0].closest('[role="button"]')
|
|
).not.toHaveTextContent('Last updated:');
|
|
|
|
const homeToolbar = document.querySelector(
|
|
'[data-home-spaces-toolbar]'
|
|
) as HTMLElement;
|
|
await user.click(within(homeToolbar).getByRole('tab', { name: 'List' }));
|
|
const listWorkspaceButtons = await waitFor(() => {
|
|
const buttons = Array.from(
|
|
document.querySelectorAll<HTMLButtonElement>(
|
|
'[data-home-space-open-workspace][data-layout="list"]'
|
|
)
|
|
);
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
return buttons;
|
|
});
|
|
expect(listWorkspaceButtons[0]).toHaveClass(
|
|
'justify-self-end',
|
|
'cursor-pointer',
|
|
'rounded-lg',
|
|
'font-medium'
|
|
);
|
|
expect(listWorkspaceButtons[0]).toHaveAttribute('data-variant', 'ghost');
|
|
expect(listWorkspaceButtons[0].parentElement?.lastElementChild).toBe(
|
|
listWorkspaceButtons[0]
|
|
);
|
|
await user.click(within(homeToolbar).getByRole('tab', { name: 'Grid' }));
|
|
});
|
|
|
|
it('keeps reduced-motion navigation to fades and keyboard navigation instant', async () => {
|
|
pageMotionMocks.reduced = true;
|
|
const user = userEvent.setup();
|
|
const now = Date.now();
|
|
useSpaceStore.setState((state) => ({
|
|
...state,
|
|
spaces: {
|
|
...state.spaces,
|
|
'motion-space': {
|
|
id: 'motion-space',
|
|
name: 'Motion Space',
|
|
description: 'Motion test space',
|
|
sourceType: 'folder',
|
|
rootPath: '/work/motion-space',
|
|
status: 'active',
|
|
schemaVersion: 1,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
},
|
|
projectsBySpaceId: {
|
|
...state.projectsBySpaceId,
|
|
'motion-space': {},
|
|
},
|
|
projectsSyncedAt: {
|
|
...state.projectsSyncedAt,
|
|
'motion-space': now,
|
|
},
|
|
}));
|
|
|
|
renderSettingsPage('/home?section=spaces');
|
|
|
|
const spaceCard = (await screen.findByText('Motion Space')).closest(
|
|
'[role="button"]'
|
|
) as HTMLElement;
|
|
await user.click(spaceCard);
|
|
|
|
const detailSidebar = await screen.findByRole('complementary', {
|
|
name: 'Spaces',
|
|
});
|
|
expect(
|
|
document.querySelector('[data-home-space-sidebar-pane="detail"]')
|
|
).toHaveAttribute('data-space-navigation-motion', 'fade');
|
|
expect(
|
|
document.querySelector('[data-home-space-content-pane="detail"]')
|
|
).toHaveAttribute('data-space-navigation-motion', 'fade');
|
|
|
|
const backButton = within(detailSidebar).getByRole('button', {
|
|
name: 'Back to Home',
|
|
});
|
|
backButton.focus();
|
|
await user.keyboard('{Enter}');
|
|
await screen.findByRole('complementary', { name: 'Home' });
|
|
|
|
expect(
|
|
document.querySelector('[data-home-space-sidebar-pane="home"]')
|
|
).toHaveAttribute('data-space-navigation-motion', 'instant');
|
|
expect(
|
|
document.querySelector('[data-home-space-content-pane="home"]')
|
|
).toHaveAttribute('data-space-navigation-motion', 'instant');
|
|
});
|
|
|
|
it('redirects an empty placeholder Space to Home without listing it', async () => {
|
|
const now = Date.now();
|
|
useSpaceStore.setState((state) => ({
|
|
...state,
|
|
spaces: {
|
|
...state.spaces,
|
|
'empty-space': {
|
|
id: 'empty-space',
|
|
name: 'Untitled Space',
|
|
sourceType: 'blank',
|
|
status: 'active',
|
|
schemaVersion: 2,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
metadata: {
|
|
createdFrom: 'space_detail_sidebar',
|
|
autoCreatedPlaceholder: true,
|
|
},
|
|
},
|
|
},
|
|
projectsBySpaceId: {
|
|
...state.projectsBySpaceId,
|
|
'empty-space': {},
|
|
},
|
|
projectsSyncedAt: {
|
|
...state.projectsSyncedAt,
|
|
'empty-space': now,
|
|
},
|
|
}));
|
|
|
|
renderSettingsPage(
|
|
'/home?section=spaces&spaceId=empty-space&spaceTab=projects'
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole('complementary', { name: 'Home' })
|
|
).toBeInTheDocument();
|
|
});
|
|
expect(screen.queryByText('Untitled Space')).not.toBeInTheDocument();
|
|
expect(
|
|
document.querySelector('[data-home-spaces-list]')
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.queryByRole('button', { name: 'Connect to a local folder' })
|
|
).not.toBeInTheDocument();
|
|
expect(document.querySelector('[data-space-stat]')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('supports horizontal section content while defaulting to vertical', () => {
|
|
const { rerender } = render(
|
|
<SettingsSection title="Section title">
|
|
<span>Section content</span>
|
|
</SettingsSection>
|
|
);
|
|
|
|
const getSectionBox = () =>
|
|
screen.getByText('Section title').parentElement?.nextElementSibling;
|
|
|
|
expect(getSectionBox()).toHaveClass('flex-col');
|
|
|
|
rerender(
|
|
<SettingsSection title="Section title" variant="horizontal">
|
|
<span>Section content</span>
|
|
</SettingsSection>
|
|
);
|
|
|
|
expect(getSectionBox()).toHaveClass('flex-row');
|
|
|
|
rerender(
|
|
<SettingsSection titleVariant="hidden">
|
|
<span>Section content</span>
|
|
</SettingsSection>
|
|
);
|
|
|
|
expect(screen.queryByText('Section title')).not.toBeInTheDocument();
|
|
expect(screen.getByText('Section content').parentElement).toHaveClass(
|
|
'rounded-2xl',
|
|
'border-0',
|
|
'bg-ds-bg-neutral-default-default',
|
|
'p-4'
|
|
);
|
|
});
|
|
});
|