Newer
Older
navi-1 / webclient / tests / unit / components / appRouting.test.js
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { setActivePinia, createPinia } from 'pinia'

const loadSession = vi.fn()

vi.mock('@/stores/chat', () => ({
  useChatStore: () => ({ currentId: null, loading: false, loadSession }),
}))
vi.mock('@/stores/sessions', () => ({
  useSessionsStore: () => ({ sessions: [], nameById: {}, fetchSessions: vi.fn() }),
}))
vi.mock('@/stores/profiles', () => ({
  useProfilesStore: () => ({ selectedProfileId: null, fetchProfiles: vi.fn() }),
}))
vi.mock('@/stores/auth', () => ({
  useAuthStore: () => ({
    authConfigured: false,
    isAuthenticated: true,
    loading: false,
    fetchStatus: vi.fn(),
    fetchMe: vi.fn(),
  }),
}))
vi.mock('gnexus-ui-kit/vue', () => ({
  GnToastProvider: { name: 'GnToastProvider', template: '<div><slot /></div>' },
}))

// Factories are hoisted above the module they mock, so each stub is written
// out in full instead of going through a helper.
vi.mock('@/components/sidebar/AppSidebar.vue', () => ({
  default: { name: 'AppSidebar', template: '<div data-test="sidebar" />' },
}))
vi.mock('@/components/ui/ConfirmDialog.vue', () => ({
  default: { name: 'ConfirmDialog', template: '<div data-test="confirm" />' },
}))
vi.mock('@/components/ui/ImageLightbox.vue', () => ({
  default: { name: 'ImageLightbox', template: '<div data-test="lightbox" />' },
}))
vi.mock('@/components/ui/SelectionToolbar.vue', () => ({
  default: { name: 'SelectionToolbar', template: '<div data-test="selection" />' },
}))
vi.mock('@/components/ui/OfflineBanner.vue', () => ({
  default: { name: 'OfflineBanner', template: '<div data-test="offline" />' },
}))
vi.mock('@/components/ui/LoginScreen.vue', () => ({
  default: { name: 'LoginScreen', template: '<div data-test="login" />' },
}))
vi.mock('@/components/ui/WelcomeScreen.vue', () => ({
  default: { name: 'WelcomeScreen', template: '<div data-test="welcome" />' },
}))
vi.mock('@/components/chat/ChatArea.vue', () => ({
  default: { name: 'ChatArea', template: '<div data-test="chat" />' },
}))
vi.mock('@/components/artifacts/ArtifactsPanel.vue', () => ({
  default: { name: 'ArtifactsPanel', template: '<div data-test="artifacts" />' },
}))
vi.mock('@/components/settings/SettingsView.vue', () => ({
  default: { name: 'SettingsView', template: '<div data-test="settings" />' },
}))

import App from '@/App.vue'

function setHash(hash) {
  history.replaceState(null, '', hash ?? location.pathname)
}

async function mountAt(hash) {
  setHash(hash)
  const wrapper = mount(App)
  await flushPromises()
  return wrapper
}

// `#settings/app` names a settings section, not a session. If the hash parser
// ever loses that distinction the app quietly tries to open a session whose id
// is "settings/app" — an empty chat instead of the settings screen.
describe('routing on the hash', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    loadSession.mockReset()
  })

  afterEach(setHash)

  it('routes a sectioned settings hash to Settings', async () => {
    const wrapper = await mountAt('#settings/app')

    expect(wrapper.find('[data-test="settings"]').exists()).toBe(true)
    expect(wrapper.find('[data-test="chat"]').exists()).toBe(false)
    expect(loadSession).not.toHaveBeenCalled()
  })

  it('routes a bare settings hash to Settings', async () => {
    const wrapper = await mountAt('#settings')

    expect(wrapper.find('[data-test="settings"]').exists()).toBe(true)
    expect(loadSession).not.toHaveBeenCalled()
  })

  it('still opens a session for a session id', async () => {
    const wrapper = await mountAt('#3f9c2b1e-4d6a-4a1f-9c2e-8b7d0e5a1c33')

    expect(loadSession).toHaveBeenCalledWith('3f9c2b1e-4d6a-4a1f-9c2e-8b7d0e5a1c33')
    expect(wrapper.find('[data-test="settings"]').exists()).toBe(false)
  })

  it('leaves settings when the hash becomes a session id', async () => {
    const wrapper = await mountAt('#settings/mcp')
    expect(wrapper.find('[data-test="settings"]').exists()).toBe(true)

    setHash('#3f9c2b1e-4d6a-4a1f-9c2e-8b7d0e5a1c33')
    window.dispatchEvent(new Event('hashchange'))
    await flushPromises()

    expect(loadSession).toHaveBeenCalledWith('3f9c2b1e-4d6a-4a1f-9c2e-8b7d0e5a1c33')
  })
})