import {describe, expect, it} from 'vitest' import {mount} from '@vue/test-utils' import type {DefineComponent} from 'vue' import {Icon as IconifyIcon} from '@iconify/vue' import Sidebar from './Sidebar.vue' type SidebarItem = { label: string to: string } 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 mountComponent(props: SidebarProps, slots?: Record) { return mount(SidebarForTest, { props, slots, global: {stubs}, }) } describe('MalioSidebar', () => { it('renders expanded by default', () => { const wrapper = mountComponent({sections}) const aside = wrapper.find('aside') expect(aside.classes()).toContain('w-[280px]') }) 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('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') }) })