import { beforeEach, describe, expect, it } from 'vitest'; import { fireEvent, render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { Sidebar } from '@/components/layout/Sidebar'; import { useChatStore } from '@/stores/chat'; import { useCronStore } from '@/stores/cron'; import { useGatewayStore } from '@/stores/gateway'; import { useSettingsStore } from '@/stores/settings'; import { useTaskCenterStore } from '@/stores/task-center'; import { useYinianStore } from '@/stores/yinian'; import i18n from '@/i18n'; const hotel = { id: 'hotel-1', name: '智念空间', city: '杭州', role: 'manager' as const, permissions: [], ota: [], }; function renderSidebar(initialEntry = '/chat') { return render( , ); } function getTestIdOrder(): string[] { const sidebar = screen.getByTestId('sidebar'); return [...sidebar.querySelectorAll('[data-testid]')] .map((element) => element.getAttribute('data-testid')) .filter((value): value is string => Boolean(value)); } describe('Sidebar layout', () => { beforeEach(async () => { window.localStorage.clear(); await i18n.changeLanguage('zh'); useSettingsStore.setState({ sidebarCollapsed: false, devModeUnlocked: false, }); useGatewayStore.setState({ status: { state: 'stopped', port: 18789, gatewayReady: false }, }); useYinianStore.setState({ status: 'authenticated', session: { authenticated: true, user: { id: 'user-1', name: '王管理员' }, hotels: [hotel], currentHotelId: hotel.id, accessTokenExpiresAt: 100, }, config: { serverTime: 1, user: { id: 'user-1', name: '王管理员' }, hotel, hotels: [hotel], entitlements: [], notificationChannels: [], featureFlags: {}, uiPolicy: { defaultPage: 'today', showAdvancedSettings: false }, }, error: null, }); useChatStore.setState({ currentSessionKey: 'agent:main:main', currentAgentId: 'main', sessions: [{ key: 'agent:main:main', displayName: 'main' }], messages: [], sessionLabels: {}, sessionLastActivity: {}, sending: false, activeRunId: null, activeRunSessionKey: null, streamingText: '', streamingMessage: null, streamingTools: [], pendingFinal: false, lastUserMessageAt: null, pendingToolImages: [], error: null, loading: false, }); useCronStore.setState({ jobs: Array.from({ length: 6 }, (_, index) => ({ id: `cron-${index + 1}`, name: `快捷任务 ${index + 1}`, message: `执行快捷任务 ${index + 1}`, schedule: '0 9 * * *', enabled: true, createdAt: '2026-05-13T00:00:00.000Z', updatedAt: '2026-05-13T00:00:00.000Z', agentId: 'main', })), loading: false, error: null, }); useTaskCenterStore.setState({ scheduledBindings: {}, runRecords: [], pinnedTaskIds: ['cron-1', 'cron-2', 'cron-3', 'cron-4', 'cron-5', 'cron-6'], }); }); it('places chat first, keeps pinned tasks under task center, and keeps account identity in the footer', () => { renderSidebar(); const order = getTestIdOrder(); const indexOf = (testId: string) => order.indexOf(testId); expect(order[0]).toBe('sidebar-new-chat'); expect(indexOf('sidebar-new-chat')).toBeLessThan(indexOf('sidebar-collapse-toggle')); expect(indexOf('sidebar-collapse-toggle')).toBeLessThan(indexOf('sidebar-chat-history')); expect(indexOf('sidebar-new-chat')).toBeLessThan(indexOf('sidebar-chat-history')); expect(indexOf('sidebar-chat-history')).toBeLessThan(indexOf('sidebar-nav-tasks')); expect(indexOf('sidebar-nav-tasks')).toBeLessThan(indexOf('sidebar-pinned-task-cron-1')); expect(indexOf('sidebar-pinned-task-cron-5')).toBeLessThan(indexOf('sidebar-pinned-task-more')); expect(indexOf('sidebar-pinned-task-more')).toBeLessThan(indexOf('sidebar-nav-app-center')); expect(indexOf('sidebar-nav-app-center')).toBeLessThan(indexOf('sidebar-nav-knowledge')); expect(indexOf('sidebar-nav-knowledge')).toBeLessThan(indexOf('sidebar-account-summary')); expect(indexOf('sidebar-account-summary')).toBeLessThan(indexOf('sidebar-account-identity')); expect(indexOf('sidebar-account-identity')).toBeLessThan(indexOf('sidebar-nav-settings')); expect(screen.queryByTestId('sidebar-pinned-task-cron-6')).not.toBeInTheDocument(); expect(screen.getByTestId('sidebar-pinned-task-more')).toHaveTextContent('查看更多 +1'); expect(screen.getByTestId('sidebar-pinned-task-more')).toHaveAttribute('href', '/tasks'); const newChat = screen.getByTestId('sidebar-new-chat'); expect(newChat).toHaveAttribute('title', '新对话'); expect(newChat.className).toContain('backdrop-blur-md'); expect(screen.getByTestId('sidebar-collapse-toggle')).toHaveAttribute('title', '收起侧栏'); expect(screen.getByTestId('sidebar-chat-history')).toHaveAttribute('title', '历史会话'); expect(screen.getByTestId('sidebar-nav-tasks')).toHaveAttribute('title', '任务中心'); expect(screen.getByTestId('sidebar-pinned-task-cron-1')).toHaveAttribute('title', '快捷任务 1'); expect(screen.getByTestId('sidebar-account-summary')).toContainElement(screen.getByTestId('sidebar-account-identity')); expect(screen.getByTestId('sidebar-account-summary')).toContainElement(screen.getByTestId('sidebar-nav-settings')); expect(screen.getByTestId('sidebar-account-workspace')).toHaveTextContent('智念空间'); expect(screen.getByTestId('sidebar-account-user')).toHaveTextContent('王管理员'); expect(screen.queryByText('对话')).not.toBeInTheDocument(); expect(screen.queryByText('任务')).not.toBeInTheDocument(); expect(screen.queryByText('快捷触发')).not.toBeInTheDocument(); expect(screen.queryByText('快速使用')).not.toBeInTheDocument(); }); it('hides pinned quick tasks when collapsed and keeps hover titles on icon buttons', () => { useSettingsStore.setState({ sidebarCollapsed: true, devModeUnlocked: false, }); renderSidebar(); expect(screen.queryByTestId('sidebar-pinned-task-cron-1')).not.toBeInTheDocument(); expect(screen.queryByTestId('sidebar-pinned-task-more')).not.toBeInTheDocument(); expect(screen.getByTestId('sidebar-new-chat')).toHaveAttribute('title', '新对话'); expect(screen.getByTestId('sidebar-collapse-toggle')).toHaveAttribute('title', '展开侧栏'); expect(screen.getByTestId('sidebar-chat-history')).toHaveAttribute('title', '历史会话'); expect(screen.getByTestId('sidebar-nav-tasks')).toHaveAttribute('title', '任务中心'); expect(screen.getByTestId('sidebar-nav-settings')).toHaveAttribute('title', '设置'); }); it('does not mark history active just because a new chat is open', () => { renderSidebar('/chat'); const historyButton = screen.getByTestId('sidebar-chat-history'); expect(historyButton).toHaveAttribute('aria-expanded', 'false'); expect(historyButton.className).not.toContain('ring-[#D5E8F3]/80'); fireEvent.click(historyButton); expect(historyButton).toHaveAttribute('aria-expanded', 'true'); expect(historyButton.className).toContain('ring-[#D5E8F3]/80'); }); });