Vitest hoists vi.mock()/vi.hoisted() above all imports, so the component import can sit with the other imports (import/first satisfied) without the eslint-disable-next-line directives — the mock factories only deref their refs at mount time. Honors the no-eslint-disable rule. 28/28 affected specs green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
259 lines
8.3 KiB
TypeScript
259 lines
8.3 KiB
TypeScript
/**
|
|
* AppSidebar.spec.ts — unit tests for AppSidebar composition and mobile wiring.
|
|
*
|
|
* Strategy: mount with @vue/test-utils stubs for all heavy children (SidebarHeader,
|
|
* SidebarNav, WorkspaceSwitcher, Drawer) so we test only:
|
|
* 1. Renders the 3 child components (SidebarHeader, SidebarNav, WorkspaceSwitcher).
|
|
* 2. Passes `groups` prop to SidebarNav.
|
|
* 3. Mobile Drawer v-model:visible wires to shell.mobileOpen (get path).
|
|
* 4. Drawer close (v-model:visible = false) calls shell.setMobileOpen(false).
|
|
* 5. Drawer is NOT rendered when isMobile=false (desktop); IS rendered when isMobile=true.
|
|
* 6. Desktop <aside> applies correct width class based on sidebarCollapsed.
|
|
*
|
|
* Stubs: Drawer is stubbed with a simple slot passthrough so we can inspect
|
|
* whether its `visible` prop is correctly bound to the store.
|
|
*
|
|
* @vueuse/core is mocked so we can control isMobile per test context.
|
|
*/
|
|
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { mount } from '@vue/test-utils'
|
|
import { ref } from 'vue'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useShellUiStore } from '@/stores/useShellUiStore'
|
|
import AppSidebar from '@/components-v2/layout/AppSidebar.vue'
|
|
import type { V2NavGroup } from '@/types/v2/nav'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock @vueuse/core so we can control `isMobile` per test.
|
|
// AppSidebar uses useBreakpoints(breakpointsTailwind).smaller('lg').
|
|
// Vitest hoists vi.mock() above all imports, so the mock is registered
|
|
// before AppSidebar's transitive @vueuse/core import resolves; the
|
|
// factory only dereferences mockIsMobileRef inside .smaller(), invoked
|
|
// at component-mount time (well after this const initialises).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const mockIsMobileRef = ref(false)
|
|
|
|
vi.mock('@vueuse/core', () => ({
|
|
breakpointsTailwind: {},
|
|
useBreakpoints: () => ({
|
|
smaller: (_: string) => mockIsMobileRef,
|
|
}),
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// A minimal nav group fixture
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const testGroups: V2NavGroup[] = [
|
|
{
|
|
label: 'Main',
|
|
items: [
|
|
{ id: 'dashboard', label: 'Dashboard', icon: 'tabler-home', to: { name: 'dashboard' } },
|
|
],
|
|
},
|
|
]
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stubs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* DrawerStub exposes the same `visible` prop as PrimeVue Drawer and renders
|
|
* its default slot so child components are mounted. It also emits
|
|
* `update:visible` when the close method would fire, which mirrors the
|
|
* v-model:visible contract.
|
|
*/
|
|
const DrawerStub = {
|
|
name: 'Drawer',
|
|
props: ['visible', 'position', 'pt'],
|
|
emits: ['update:visible'],
|
|
template: '<div class="drawer-stub" :data-visible="visible"><slot /></div>',
|
|
}
|
|
|
|
const globalStubs = {
|
|
Drawer: DrawerStub,
|
|
SidebarHeader: { name: 'SidebarHeader', template: '<div class="sidebar-header-stub" />' },
|
|
SidebarNav: {
|
|
name: 'SidebarNav',
|
|
props: ['groups', 'collapsed'],
|
|
template: '<div class="sidebar-nav-stub" :data-collapsed="collapsed" :data-groups-count="groups.length" />',
|
|
},
|
|
WorkspaceSwitcher: {
|
|
name: 'WorkspaceSwitcher',
|
|
props: ['collapsed'],
|
|
template: '<div class="workspace-switcher-stub" />',
|
|
},
|
|
}
|
|
|
|
function mountSidebar(groups: V2NavGroup[] = testGroups) {
|
|
return mount(AppSidebar, {
|
|
props: { groups },
|
|
global: {
|
|
plugins: [createPinia()],
|
|
stubs: globalStubs,
|
|
},
|
|
})
|
|
}
|
|
|
|
describe('AppSidebar', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
|
|
// Default to desktop (isMobile=false) so most tests exercise the stable path
|
|
mockIsMobileRef.value = false
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Child composition
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('renders SidebarHeader', () => {
|
|
const wrapper = mountSidebar()
|
|
|
|
expect(wrapper.find('.sidebar-header-stub').exists()).toBe(true)
|
|
})
|
|
|
|
it('renders SidebarNav', () => {
|
|
const wrapper = mountSidebar()
|
|
|
|
expect(wrapper.find('.sidebar-nav-stub').exists()).toBe(true)
|
|
})
|
|
|
|
it('renders WorkspaceSwitcher', () => {
|
|
const wrapper = mountSidebar()
|
|
|
|
expect(wrapper.find('.workspace-switcher-stub').exists()).toBe(true)
|
|
})
|
|
|
|
it('passes groups prop to SidebarNav', () => {
|
|
const wrapper = mountSidebar()
|
|
const nav = wrapper.find('.sidebar-nav-stub')
|
|
|
|
// Our stub renders data-groups-count from the groups prop
|
|
expect(nav.attributes('data-groups-count')).toBe(String(testGroups.length))
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Drawer v-if: mount/unmount based on isMobile
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('desktop (isMobile=false): Drawer is NOT rendered', () => {
|
|
mockIsMobileRef.value = false
|
|
|
|
const wrapper = mountSidebar()
|
|
|
|
// v-if="isMobile" means the Drawer stub must be absent on desktop
|
|
expect(wrapper.find('.drawer-stub').exists()).toBe(false)
|
|
})
|
|
|
|
it('mobile (isMobile=true): Drawer IS rendered', async () => {
|
|
mockIsMobileRef.value = true
|
|
|
|
const wrapper = mountSidebar()
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.find('.drawer-stub').exists()).toBe(true)
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Desktop <aside> width class based on sidebarCollapsed
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('desktop: aside has w-64 class when sidebar is expanded', async () => {
|
|
mockIsMobileRef.value = false
|
|
|
|
const wrapper = mountSidebar()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.sidebarCollapsed = false
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.find('aside').classes()).toContain('w-64')
|
|
expect(wrapper.find('aside').classes()).not.toContain('w-16')
|
|
})
|
|
|
|
it('desktop: aside has w-16 class when sidebar is collapsed', async () => {
|
|
mockIsMobileRef.value = false
|
|
|
|
const wrapper = mountSidebar()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.sidebarCollapsed = true
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.find('aside').classes()).toContain('w-16')
|
|
expect(wrapper.find('aside').classes()).not.toContain('w-64')
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Mobile Drawer wiring (only exercised when isMobile=true)
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('mobile: Drawer visible is true when shell.mobileOpen is true', async () => {
|
|
mockIsMobileRef.value = true
|
|
|
|
const wrapper = mountSidebar()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.mobileOpen = true
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
// The Drawer stub renders data-visible from its visible prop
|
|
const drawer = wrapper.find('.drawer-stub')
|
|
|
|
expect(drawer.attributes('data-visible')).toBe('true')
|
|
})
|
|
|
|
it('mobile: Drawer visible is false when shell.mobileOpen is false', async () => {
|
|
mockIsMobileRef.value = true
|
|
|
|
const wrapper = mountSidebar()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.mobileOpen = false
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const drawer = wrapper.find('.drawer-stub')
|
|
|
|
expect(drawer.attributes('data-visible')).toBe('false')
|
|
})
|
|
|
|
it('mobile: emitting update:visible false from Drawer calls shell.setMobileOpen(false)', async () => {
|
|
mockIsMobileRef.value = true
|
|
|
|
const wrapper = mountSidebar()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.mobileOpen = true
|
|
|
|
const setMobileOpenSpy = vi.spyOn(shell, 'setMobileOpen')
|
|
|
|
// Simulate the Drawer closing (v-model:visible setter)
|
|
await wrapper.findComponent(DrawerStub).vm.$emit('update:visible', false)
|
|
|
|
expect(setMobileOpenSpy).toHaveBeenCalledWith(false)
|
|
})
|
|
|
|
it('mobile: emitting update:visible true from Drawer calls shell.setMobileOpen(true)', async () => {
|
|
mockIsMobileRef.value = true
|
|
|
|
const wrapper = mountSidebar()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.mobileOpen = false
|
|
|
|
const setMobileOpenSpy = vi.spyOn(shell, 'setMobileOpen')
|
|
|
|
await wrapper.findComponent(DrawerStub).vm.$emit('update:visible', true)
|
|
|
|
expect(setMobileOpenSpy).toHaveBeenCalledWith(true)
|
|
})
|
|
})
|