// @vitest-environment jsdom import { beforeEach, describe, expect, it, vi } from 'vitest' import { mount } from '@vue/test-utils' const openSessionSearchMock = vi.hoisted(() => vi.fn()) const mockAppStore = vi.hoisted(() => ({ sidebarOpen: true, sidebarCollapsed: false, connected: true, serverVersion: 'test', latestVersion: '', updateAvailable: false, clientOutdated: false, updating: false, toggleSidebar: vi.fn(), toggleSidebarCollapsed: vi.fn(), closeSidebar: vi.fn(), doUpdate: vi.fn(), reloadClient: vi.fn(), })) vi.mock('@/composables/useSessionSearch', () => ({ useSessionSearch: () => ({ openSessionSearch: openSessionSearchMock, }), })) vi.mock('@/stores/hermes/app', () => ({ useAppStore: () => mockAppStore, })) vi.mock('vue-router', async (importOriginal) => { const actual = await importOriginal() return { ...actual, useRoute: () => ({ name: 'hermes.chat' }), useRouter: () => ({ push: vi.fn(), hasRoute: () => true }), } }) vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key, }), createI18n: () => ({ global: { locale: { value: 'en' }, setLocaleMessage: vi.fn() }, }), })) vi.mock('@/composables/useTheme', () => ({ useTheme: () => ({ isDark: false }), })) vi.mock('/logo.png', () => ({ default: 'logo.png', })) vi.mock('@/components/layout/ProfileSelector.vue', () => ({ default: { name: 'ProfileSelector', template: '
' }, })) vi.mock('@/components/layout/ModelSelector.vue', () => ({ default: { name: 'ModelSelector', template: '
' }, })) vi.mock('@/components/layout/LanguageSwitch.vue', () => ({ default: { name: 'LanguageSwitch', template: '
' }, })) vi.mock('@/components/layout/ThemeSwitch.vue', () => ({ default: { name: 'ThemeSwitch', template: '
' }, })) vi.mock('@/components/common/RouteLinkItem.vue', () => ({ default: { name: 'RouteLinkItem', props: ['to', 'active'], template: '', }, })) vi.mock('naive-ui', async () => { const actual = await vi.importActual('naive-ui') return { ...actual, useMessage: () => ({ success: vi.fn(), error: vi.fn(), }), NButton: { template: '', }, NSelect: { template: '
', }, } }) import AppSidebar from '@/components/layout/AppSidebar.vue' describe('AppSidebar search entry', () => { beforeEach(() => { openSessionSearchMock.mockClear() mockAppStore.serverVersion = 'test' mockAppStore.latestVersion = '' mockAppStore.updateAvailable = false mockAppStore.clientOutdated = false mockAppStore.updating = false mockAppStore.sidebarCollapsed = false mockAppStore.reloadClient.mockClear() }) it('opens the session search modal from the sidebar button', async () => { const wrapper = mount(AppSidebar, { global: { stubs: { ProfileSelector: true, ModelSelector: true, LanguageSwitch: true, ThemeSwitch: true, NButton: true, }, }, }) const buttons = wrapper.findAll('button') const searchButton = buttons.find(node => node.text().includes('sidebar.search')) expect(searchButton).toBeTruthy() await searchButton!.trigger('click') expect(openSessionSearchMock).toHaveBeenCalledTimes(1) }) it('offers a client reload when the server version differs from the loaded bundle', async () => { mockAppStore.clientOutdated = true mockAppStore.serverVersion = '0.5.17' const wrapper = mount(AppSidebar, { global: { stubs: { ProfileSelector: true, ModelSelector: true, LanguageSwitch: true, ThemeSwitch: true, }, }, }) const reloadButton = wrapper.findAll('button') .find(node => node.text().includes('sidebar.reloadClientVersion')) expect(reloadButton).toBeTruthy() await reloadButton!.trigger('click') expect(mockAppStore.reloadClient).toHaveBeenCalledTimes(1) }) it('uses short group labels and keeps group folding active when collapsed', async () => { mockAppStore.sidebarCollapsed = true const wrapper = mount(AppSidebar, { global: { stubs: { ProfileSelector: true, ModelSelector: true, LanguageSwitch: true, ThemeSwitch: true, NButton: true, }, }, }) expect(wrapper.classes()).toContain('collapsed') expect(wrapper.findAll('.nav-group-label span').map(node => node.text())).toEqual([ 'sidebar.groupConversationShort', 'sidebar.groupAgentShort', 'sidebar.groupMonitoringShort', 'sidebar.groupToolsShort', 'sidebar.groupSystemShort', ]) const agentGroup = wrapper.findAll('.nav-group')[1] expect(agentGroup.find('.nav-group-items').attributes('style')).toBeUndefined() await agentGroup.find('.nav-group-label').trigger('click') expect(agentGroup.find('.nav-group-items').attributes('style')).toContain('display: none') }) })