import {describe, expect, it} from 'vitest' import {mount} from '@vue/test-utils' import type {DefineComponent} from 'vue' import {createMemoryHistory, createRouter} from 'vue-router' import {Icon as IconifyIcon} from '@iconify/vue' import Sidebar from './Sidebar.vue' type SidebarItem = { label: string to: string exact?: boolean } type SidebarSection = { label?: string icon?: string items: SidebarItem[] } type SidebarProps = { sections: SidebarSection[] modelValue?: boolean id?: string sidebarClass?: string toggleClass?: string } const SidebarForTest = Sidebar as DefineComponent const sections: SidebarSection[] = [ { label: 'LOGISTIQUE / TRANSPORT', icon: 'mdi:truck-delivery', items: [ {label: 'Réception / Expédition', to: '/reception'}, {label: 'Validation expédition', to: '/validation'}, ], }, { label: 'COMMERCIAL', icon: 'mdi:handshake', items: [ {label: 'Répertoire Fournisseurs', to: '/fournisseurs'}, ], }, ] const stubs = { NuxtLink: { template: '', props: ['to'], }, } function makeRouter(path = '/') { const router = createRouter({ history: createMemoryHistory(), routes: [{path: '/:all(.*)*', component: {template: '
'}}], }) router.push(path) return router } function mountComponent(props: SidebarProps, slots?: Record) { return mount(SidebarForTest, { props, slots, global: {stubs, plugins: [makeRouter()]}, }) } // Monte avec le router positionné sur `path` (pour tester l'état actif). async function mountAt(path: string, props: SidebarProps = {sections}) { const router = makeRouter(path) await router.isReady() return mount(SidebarForTest, {props, global: {stubs, plugins: [router]}}) } describe('MalioSidebar', () => { it('renders expanded by default', () => { const wrapper = mountComponent({sections}) const aside = wrapper.find('aside') expect(aside.classes()).toContain('w-[232px]') }) it('renders section labels with icons when expanded', () => { const wrapper = mountComponent({sections}) const sectionHeaders = wrapper.findAll('nav > div > div') expect(sectionHeaders).toHaveLength(2) expect(sectionHeaders[0].text()).toContain('LOGISTIQUE / TRANSPORT') expect(sectionHeaders[1].text()).toContain('COMMERCIAL') }) it('renders all menu items with icons and labels', () => { const wrapper = mountComponent({sections}) const links = wrapper.findAll('a') expect(links).toHaveLength(3) expect(links[0].text()).toContain('Réception / Expédition') expect(links[1].text()).toContain('Validation expédition') expect(links[2].text()).toContain('Répertoire Fournisseurs') }) it('renders NuxtLink with correct to prop', () => { const wrapper = mountComponent({sections}) const links = wrapper.findAll('a') expect(links[0].attributes('href')).toBe('/reception') expect(links[2].attributes('href')).toBe('/fournisseurs') }) it('hover : fond + couleur + semi-bold tous portés par le
  • (texte non figé sur le )', () => { const wrapper = mountComponent({sections}) const li = wrapper.find('li') expect(li.classes()).toContain('hover:bg-m-primary/10') expect(li.classes()).toContain('hover:text-m-primary') expect(li.classes()).toContain('hover:font-semibold') expect(li.classes()).toContain('text-black') expect(li.classes()).toContain('pt-1') expect(li.classes()).toContain('pb-1') // Le ne fige PAS sa couleur (sinon le texte resterait noir sur les bandes // pt-1/pb-1 hors du alors que le fond du
  • est bleu). expect(wrapper.find('a').classes()).not.toContain('text-black') expect(wrapper.find('a').classes()).not.toContain('hover:text-m-primary') }) it('actif : route exacte → lien en primary + semi-bold, sans fond', () => { return mountAt('/reception').then((wrapper) => { const link = wrapper.findAll('a')[0] expect(link.classes()).toContain('font-semibold') expect(link.classes()).toContain('!text-m-primary') expect(link.classes().some(c => c.startsWith('bg-'))).toBe(false) }) }) it('actif : reste actif sur une sous-route (match par préfixe)', async () => { const wrapper = await mountAt('/reception/1/edit') expect(wrapper.findAll('a')[0].classes()).toContain('font-semibold') }) it('actif : les autres liens ne sont pas actifs sur une sous-route', async () => { const wrapper = await mountAt('/reception/1/edit') expect(wrapper.findAll('a')[1].classes()).not.toContain('font-semibold') }) it('exact : pas actif sur une sous-route', async () => { const exactSections: SidebarSection[] = [ {label: 'S', icon: 'mdi:home', items: [{label: 'R', to: '/reception', exact: true}]}, ] const wrapper = await mountAt('/reception/1/edit', {sections: exactSections}) expect(wrapper.find('a').classes()).not.toContain('font-semibold') }) it('exact : actif sur la route exacte', async () => { const exactSections: SidebarSection[] = [ {label: 'S', icon: 'mdi:home', items: [{label: 'R', to: '/reception', exact: true}]}, ] const wrapper = await mountAt('/reception', {sections: exactSections}) expect(wrapper.find('a').classes()).toContain('font-semibold') }) it('renders section icons via IconifyIcon', () => { const wrapper = mountComponent({sections}) const icons = wrapper.findAllComponents(IconifyIcon) // 2 section icons + 1 toggle chevron = 3 expect(icons).toHaveLength(3) expect(icons[0].props('icon')).toBe('mdi:truck-delivery') expect(icons[1].props('icon')).toBe('mdi:handshake') }) it('toggle button shows chevron-left when expanded', () => { const wrapper = mountComponent({sections}) const toggleIcon = wrapper.findAllComponents(IconifyIcon).at(-1)! expect(toggleIcon.props('icon')).toBe('mdi:chevron-left') }) it('collapses on toggle click in uncontrolled mode', async () => { const wrapper = mountComponent({sections}) const toggleBtn = wrapper.find('button') await toggleBtn.trigger('click') const aside = wrapper.find('aside') expect(aside.classes()).toContain('w-[72px]') }) it('hides section label text when collapsed but keeps section icon', async () => { const wrapper = mountComponent({sections}) await wrapper.find('button').trigger('click') const sectionHeaders = wrapper.findAll('nav > div > div') expect(sectionHeaders).toHaveLength(2) // Label text spans are hidden sectionHeaders.forEach((header) => { expect(header.findAll('span').filter(s => s.classes().includes('text-[11px]'))).toHaveLength(0) }) }) it('hides item text when collapsed', async () => { const wrapper = mountComponent({sections}) await wrapper.find('button').trigger('click') const itemSpans = wrapper.findAll('a span') expect(itemSpans).toHaveLength(0) }) it('toggle button shows chevron-right when collapsed', async () => { const wrapper = mountComponent({sections}) await wrapper.find('button').trigger('click') const toggleIcon = wrapper.findAllComponents(IconifyIcon).at(-1)! expect(toggleIcon.props('icon')).toBe('mdi:chevron-right') }) it('emits update:modelValue on toggle click', async () => { const wrapper = mountComponent({sections, modelValue: false}) await wrapper.find('button').trigger('click') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([true]) }) it('respects modelValue in controlled mode', () => { const wrapper = mountComponent({sections, modelValue: true}) const aside = wrapper.find('aside') expect(aside.classes()).toContain('w-[72px]') }) it('renders logo slot when expanded', () => { const wrapper = mountComponent({sections}, { logo: 'Malio', }) expect(wrapper.find('img[alt="Malio"]').exists()).toBe(true) }) it('renders logo-collapsed slot when collapsed', async () => { const wrapper = mountComponent({sections}, { 'logo-collapsed': 'M', }) await wrapper.find('button').trigger('click') expect(wrapper.find('img[alt="M"]').exists()).toBe(true) }) it('uses custom id when provided', () => { const wrapper = mountComponent({sections, id: 'my-sidebar'}) expect(wrapper.find('aside').attributes('id')).toBe('my-sidebar') }) it('toggle button has correct aria-label', async () => { const wrapper = mountComponent({sections}) const btn = wrapper.find('button') expect(btn.attributes('aria-label')).toBe('Plier le menu') await btn.trigger('click') expect(btn.attributes('aria-label')).toBe('Déplier le menu') }) it('section without label does not render a section header', () => { const noLabelSections: SidebarSection[] = [ {items: [{label: 'Item', to: '/'}]}, ] const wrapper = mountComponent({sections: noLabelSections}) expect(wrapper.findAll('nav > div > div')).toHaveLength(0) }) it('renders section icon in collapsed mode', async () => { const wrapper = mountComponent({sections}) await wrapper.find('button').trigger('click') const icons = wrapper.findAllComponents(IconifyIcon) // 2 section icons + 1 toggle = 3 expect(icons[0].props('icon')).toBe('mdi:truck-delivery') expect(icons[1].props('icon')).toBe('mdi:handshake') }) })