Adds components/shared/ContextSwitcher.vue — a Vuetify menu-button that renders only when useAuthStore.showContextSwitcher is true (i.e. the user has both portal and organizer contexts available). Click calls useAuthStore.setLastContext + resolveLandingRoute and pushes the new route. Wired into both layouts: - PortalLayout.vue: navbar right section, before UserAvatarMenu - DefaultLayoutWithVerticalNav.vue (organizer navbar host): before NavbarThemeSwitcher (OrganizerLayout.vue itself is a 10-line wrapper around DefaultLayoutWithVerticalNav, so the component wires into the actual navbar host). Boundaries matrix update: components-shared now allows `stores` so canonical shared chrome (ContextSwitcher, future global indicators) can read useAuthStore directly without re-homing to components/layout/. stores-portal stays disallowed for components- shared by design — portal-specific state has no place in shared chrome. Adds 3 vitest specs covering: visibility gated by showContextSwitcher, click invokes setLastContext + router.push. Test count 189 → 192. Frontend lint + typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
|
|
const mockSetLastContext = vi.fn()
|
|
const mockResolveLandingRoute = vi.fn(() => ({ path: '/portal/evenementen' }))
|
|
const mockPush = vi.fn()
|
|
|
|
const authStoreState: Record<string, unknown> = {
|
|
showContextSwitcher: true,
|
|
setLastContext: mockSetLastContext,
|
|
resolveLandingRoute: mockResolveLandingRoute,
|
|
}
|
|
|
|
vi.mock('@/stores/useAuthStore', () => ({
|
|
useAuthStore: () => authStoreState,
|
|
}))
|
|
|
|
vi.mock('vue-router', async importOriginal => ({
|
|
...(await importOriginal<typeof import('vue-router')>()),
|
|
useRoute: () => ({ meta: { context: 'organizer' } }),
|
|
useRouter: () => ({ push: mockPush }),
|
|
}))
|
|
|
|
const ContextSwitcher = (await import('../ContextSwitcher.vue')).default
|
|
|
|
const stubs = {
|
|
VMenu: { template: '<div data-test="menu"><slot name="activator" :props="{}" /><slot /></div>' },
|
|
VBtn: { template: '<button><slot /></button>' },
|
|
VList: { template: '<div><slot /></div>' },
|
|
VListItem: {
|
|
props: ['dataTest'],
|
|
template: '<div :data-test="dataTest" @click="$emit(\'click\')"><slot /></div>',
|
|
emits: ['click'],
|
|
},
|
|
VListItemTitle: { template: '<span><slot /></span>' },
|
|
VIcon: true,
|
|
}
|
|
|
|
describe('ContextSwitcher', () => {
|
|
it('renders when showContextSwitcher is true', () => {
|
|
authStoreState.showContextSwitcher = true
|
|
|
|
const wrapper = mount(ContextSwitcher, { global: { stubs } })
|
|
|
|
expect(wrapper.find('[data-test="menu"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('does not render when showContextSwitcher is false (single-context user)', () => {
|
|
authStoreState.showContextSwitcher = false
|
|
|
|
const wrapper = mount(ContextSwitcher, { global: { stubs } })
|
|
|
|
expect(wrapper.find('[data-test="menu"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('clicking the alternative context calls setLastContext and router.push', async () => {
|
|
authStoreState.showContextSwitcher = true
|
|
mockSetLastContext.mockClear()
|
|
mockResolveLandingRoute.mockClear()
|
|
mockPush.mockClear()
|
|
|
|
const wrapper = mount(ContextSwitcher, { global: { stubs } })
|
|
|
|
// current context is 'organizer' (from mocked route.meta.context),
|
|
// so the dropdown shows 'switch-to-portal'.
|
|
await wrapper.find('[data-test="switch-to-portal"]').trigger('click')
|
|
|
|
expect(mockSetLastContext).toHaveBeenCalledWith('portal')
|
|
expect(mockResolveLandingRoute).toHaveBeenCalledWith('portal')
|
|
expect(mockPush).toHaveBeenCalledWith({ path: '/portal/evenementen' })
|
|
})
|
|
})
|