Ports crewli-starter's monolithic AppSidebar.vue into two typed production components: SidebarHeader (the .brand block) and AppSidebar (composing SidebarHeader + SidebarNav + WorkspaceSwitcher). AppSidebar renders a permanent <aside> on desktop (lg+) and a PrimeVue Drawer on mobile, both wired to useShellUiStore for collapse/mobile state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
168 lines
5.3 KiB
TypeScript
168 lines
5.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).
|
|
*
|
|
* Stubs: Drawer is stubbed with a simple slot passthrough so we can inspect
|
|
* whether its `visible` prop is correctly bound to the store.
|
|
*/
|
|
|
|
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 AppSidebar from '@/components-v2/layout/AppSidebar.vue'
|
|
import type { V2NavGroup } from '@/types/v2/nav'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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()))
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 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))
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Mobile Drawer wiring
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('Drawer visible is true when shell.mobileOpen is true', async () => {
|
|
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('Drawer visible is false when shell.mobileOpen is false', async () => {
|
|
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('emitting update:visible false from Drawer calls shell.setMobileOpen(false)', async () => {
|
|
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('emitting update:visible true from Drawer calls shell.setMobileOpen(true)', async () => {
|
|
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)
|
|
})
|
|
})
|