/**
* AppTopbar.spec.ts
*
* Strategy: mount with createPinia(); stub all PrimeVue components so we
* test only the wiring of store actions to user interactions.
*
* Assertions:
* 1. Hamburger click → shell.setMobileOpen(true)
* 2. Theme toggle click → shell.setTheme with flipped value (light→dark, dark→light)
* 3. Density toggle click → shell.setDensity with flipped value
* 4. User-menu Sign out command → authStore.logout called
*
* useBreadcrumb() calls useRoute() internally. We provide a minimal
* vue-router mock via vi.mock so the composable has a route to call.
*/
import { createPinia, setActivePinia } from 'pinia'
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useShellUiStore } from '@/stores/useShellUiStore'
import { useAuthStore } from '@/stores/useAuthStore'
// ---------------------------------------------------------------------------
// Mock vue-router (useBreadcrumb calls useRoute internally)
// ---------------------------------------------------------------------------
vi.mock('vue-router', () => ({
useRoute: () => ({
matched: [],
}),
useRouter: () => ({}),
}))
// ---------------------------------------------------------------------------
// Import component AFTER mocks
// ---------------------------------------------------------------------------
// eslint-disable-next-line import/first -- intentional: mocks must precede import
import AppTopbar from '@/components-v2/layout/AppTopbar.vue'
// ---------------------------------------------------------------------------
// Stubs for PrimeVue components
// ---------------------------------------------------------------------------
const globalStubs = {
Breadcrumb: { name: 'Breadcrumb', props: ['model'], template: '' },
InputText: { name: 'InputText', template: '' },
OverlayBadge: { name: 'OverlayBadge', props: ['value', 'severity'], template: '
' },
Popover: {
name: 'Popover',
template: '
',
methods: { toggle: vi.fn(), hide: vi.fn() },
},
Avatar: {
name: 'Avatar',
props: ['label', 'shape', 'pt'],
template: '{{ label }}
',
emits: ['click'],
},
Menu: {
name: 'Menu',
props: ['model', 'popup'],
template: '',
methods: { toggle: vi.fn(), hide: vi.fn() },
},
Icon: { name: 'Icon', props: ['name', 'size'], template: '' },
}
// ---------------------------------------------------------------------------
// Mount helper
// ---------------------------------------------------------------------------
function mountTopbar() {
return mount(AppTopbar, {
global: {
plugins: [createPinia()],
stubs: globalStubs,
},
})
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('AppTopbar', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
// -------------------------------------------------------------------------
// Hamburger → setMobileOpen(true)
// -------------------------------------------------------------------------
it('hamburger button click calls shell.setMobileOpen(true)', async () => {
const wrapper = mountTopbar()
const shell = useShellUiStore()
const spy = vi.spyOn(shell, 'setMobileOpen')
// The hamburger is the first button (aria-label="Open menu")
const hamburger = wrapper.find('button[aria-label="Open menu"]')
expect(hamburger.exists()).toBe(true)
await hamburger.trigger('click')
expect(spy).toHaveBeenCalledWith(true)
})
// -------------------------------------------------------------------------
// Theme toggle → setTheme with flipped value
// -------------------------------------------------------------------------
it('theme toggle flips light → dark', async () => {
const wrapper = mountTopbar()
const shell = useShellUiStore()
shell.theme = 'light'
const spy = vi.spyOn(shell, 'setTheme')
const themeBtn = wrapper.find('button[aria-label="Switch to dark mode"]')
expect(themeBtn.exists()).toBe(true)
await themeBtn.trigger('click')
expect(spy).toHaveBeenCalledWith('dark')
})
it('theme toggle flips dark → light', async () => {
const wrapper = mountTopbar()
const shell = useShellUiStore()
shell.theme = 'dark'
const spy = vi.spyOn(shell, 'setTheme')
// After setting dark we need nextTick so the aria-label computed updates
await wrapper.vm.$nextTick()
const themeBtn = wrapper.find('button[aria-label="Switch to light mode"]')
expect(themeBtn.exists()).toBe(true)
await themeBtn.trigger('click')
expect(spy).toHaveBeenCalledWith('light')
})
// -------------------------------------------------------------------------
// Density toggle → setDensity with flipped value
// -------------------------------------------------------------------------
it('density toggle flips comfortable → compact', async () => {
const wrapper = mountTopbar()
const shell = useShellUiStore()
shell.density = 'comfortable'
const spy = vi.spyOn(shell, 'setDensity')
const densityBtn = wrapper.find('button[aria-label="Switch to compact"]')
expect(densityBtn.exists()).toBe(true)
await densityBtn.trigger('click')
expect(spy).toHaveBeenCalledWith('compact')
})
it('density toggle flips compact → comfortable', async () => {
const wrapper = mountTopbar()
const shell = useShellUiStore()
shell.density = 'compact'
const spy = vi.spyOn(shell, 'setDensity')
await wrapper.vm.$nextTick()
const densityBtn = wrapper.find('button[aria-label="Switch to comfortable"]')
expect(densityBtn.exists()).toBe(true)
await densityBtn.trigger('click')
expect(spy).toHaveBeenCalledWith('comfortable')
})
// -------------------------------------------------------------------------
// Sign out → authStore.logout()
// -------------------------------------------------------------------------
it('Sign out menu item command calls authStore.logout()', async () => {
const wrapper = mountTopbar()
const authStore = useAuthStore()
// Spy on logout — stub the async to resolve immediately
const logoutSpy = vi.spyOn(authStore, 'logout').mockResolvedValue()
// Access the computed userMenuItems exposed on the component's vm
// The menu items are passed as :model to the stubbed Menu component.
// We need to find the Sign out item and call its command directly.
const menuStub = wrapper.findComponent({ name: 'Menu' })
expect(menuStub.exists()).toBe(true)
// The model prop is the computed userMenuItems array
const model = menuStub.props('model') as Array<{ items: Array<{ label: string; command?: () => void }> }>
// Find Sign out in the nested items groups
const signOutItem = model
.flatMap(group => group.items ?? [])
.find(item => item.label === 'Sign out')
expect(signOutItem).toBeDefined()
expect(typeof signOutItem?.command).toBe('function')
signOutItem?.command?.()
expect(logoutSpy).toHaveBeenCalledOnce()
})
})