251c939ba0
Release / release (push) Successful in 48s
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié --------- Co-authored-by: admin malio <malio@yuno.malio.fr> Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-authored-by: matthieu <matthieu@yuno.malio.fr> Reviewed-on: #82 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
274 lines
9.3 KiB
TypeScript
274 lines
9.3 KiB
TypeScript
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<SidebarProps>
|
|
|
|
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: '<a :href="to" v-bind="$attrs"><slot /></a>',
|
|
props: ['to'],
|
|
},
|
|
}
|
|
|
|
function makeRouter(path = '/') {
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [{path: '/:all(.*)*', component: {template: '<div />'}}],
|
|
})
|
|
router.push(path)
|
|
return router
|
|
}
|
|
|
|
function mountComponent(props: SidebarProps, slots?: Record<string, string>) {
|
|
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 <li> (texte non figé sur le <a>)', () => {
|
|
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 <a> ne fige PAS sa couleur (sinon le texte resterait noir sur les bandes
|
|
// pt-1/pb-1 hors du <a> alors que le fond du <li> 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: '<img alt="Malio" src="/logo.svg" />',
|
|
})
|
|
expect(wrapper.find('img[alt="Malio"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('renders logo-collapsed slot when collapsed', async () => {
|
|
const wrapper = mountComponent({sections}, {
|
|
'logo-collapsed': '<img alt="M" src="/logo-m.svg" />',
|
|
})
|
|
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')
|
|
})
|
|
})
|