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 Drawer from './Drawer.vue' type DrawerProps = { id?: string modelValue?: boolean side?: 'right' | 'left' showClose?: boolean dismissable?: boolean closeOnEscape?: boolean ariaLabel?: string drawerClass?: string overlayClass?: string headerClass?: string bodyClass?: string footerClass?: string } const DrawerForTest = Drawer as DefineComponent function mountComponent(props: DrawerProps = {}, slots?: Record) { return mount(DrawerForTest, { props, slots, global: { stubs: { Teleport: true } }, }) } describe('MalioDrawer', () => { it('does not render when modelValue is false', () => { const wrapper = mountComponent({ modelValue: false }) expect(wrapper.find('[data-test="panel"]').exists()).toBe(false) }) it('renders the panel when modelValue is true', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('[data-test="panel"]').exists()).toBe(true) }) it('renders default slot in the body', () => { const wrapper = mountComponent( { modelValue: true }, { default: '

Contenu

' }, ) expect(wrapper.find('[data-test="body"] [data-test="content"]').text()).toBe('Contenu') }) it('works in uncontrolled mode (defaults closed)', () => { const wrapper = mountComponent() expect(wrapper.find('[data-test="panel"]').exists()).toBe(false) }) it('uses custom id when provided', () => { const wrapper = mountComponent({ modelValue: true, id: 'my-drawer' }) expect(wrapper.find('.fixed').attributes('id')).toBe('my-drawer') }) it('generates an id when not provided', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('.fixed').attributes('id')).toMatch(/^malio-drawer-/) }) it('has role="dialog" and aria-modal on the panel', () => { const wrapper = mountComponent({ modelValue: true }) const panel = wrapper.find('[data-test="panel"]') expect(panel.attributes('role')).toBe('dialog') expect(panel.attributes('aria-modal')).toBe('true') }) it('applies drawerClass to the panel', () => { const wrapper = mountComponent({ modelValue: true, drawerClass: 'max-w-2xl' }) expect(wrapper.find('[data-test="panel"]').classes()).toContain('max-w-2xl') }) it('renders the #header slot inside the header bar', () => { const wrapper = mountComponent( { modelValue: true }, { header: '

Titre

' }, ) expect(wrapper.find('[data-test="header"] [data-test="title"]').text()).toBe('Titre') }) it('renders the header bar when showClose is true even without #header', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('[data-test="header"]').exists()).toBe(true) }) it('does not render the header bar when no #header and showClose is false', () => { const wrapper = mountComponent({ modelValue: true, showClose: false }) expect(wrapper.find('[data-test="header"]').exists()).toBe(false) }) it('shows the close button by default', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('[data-test="close-button"]').exists()).toBe(true) }) it('hides the close button when showClose is false', () => { const wrapper = mountComponent( { modelValue: true, showClose: false }, { header: '

Titre

' }, ) expect(wrapper.find('[data-test="close-button"]').exists()).toBe(false) }) it('close button renders mdi:cancel-bold icon', () => { const wrapper = mountComponent({ modelValue: true }) const icon = wrapper.findComponent(IconifyIcon) expect(icon.props('icon')).toBe('mdi:cancel-bold') }) it('close button has aria-label "Fermer"', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('[data-test="close-button"]').attributes('aria-label')).toBe('Fermer') }) it('emits update:modelValue false and close on close button click', async () => { const wrapper = mountComponent({ modelValue: true }) await wrapper.find('[data-test="close-button"]').trigger('click') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([false]) expect(wrapper.emitted('close')).toHaveLength(1) }) it('sets aria-labelledby to the header id when #header is provided', () => { const wrapper = mountComponent( { modelValue: true, id: 'test-drawer' }, { header: '

Titre

' }, ) const panel = wrapper.find('[data-test="panel"]') expect(panel.attributes('aria-labelledby')).toBe('test-drawer-header') expect(wrapper.find('[data-test="header-content"]').attributes('id')).toBe('test-drawer-header') }) it('sets aria-label from ariaLabel when no #header is provided', () => { const wrapper = mountComponent({ modelValue: true, ariaLabel: 'Panneau latéral' }) const panel = wrapper.find('[data-test="panel"]') expect(panel.attributes('aria-label')).toBe('Panneau latéral') expect(panel.attributes('aria-labelledby')).toBeUndefined() }) it('applies headerClass to the header bar', () => { const wrapper = mountComponent({ modelValue: true, headerClass: 'bg-m-primary' }) expect(wrapper.find('[data-test="header"]').classes()).toContain('bg-m-primary') }) it('renders the #footer slot inside the body (scrollable zone)', () => { const wrapper = mountComponent( { modelValue: true }, { footer: '' }, ) expect(wrapper.find('[data-test="body"] [data-test="footer"] [data-test="save"]').exists()).toBe(true) }) it('does not render the footer wrapper when no #footer slot', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('[data-test="footer"]').exists()).toBe(false) }) it('applies bodyClass to the body', () => { const wrapper = mountComponent({ modelValue: true, bodyClass: 'px-10' }) expect(wrapper.find('[data-test="body"]').classes()).toContain('px-10') }) it('applies footerClass to the footer wrapper', () => { const wrapper = mountComponent( { modelValue: true, footerClass: 'sticky bottom-0' }, { footer: 'pied' }, ) const footer = wrapper.find('[data-test="footer"]') expect(footer.classes()).toContain('sticky') expect(footer.classes()).toContain('bottom-0') }) it('aligns to the right by default', () => { const wrapper = mountComponent({ modelValue: true }) expect(wrapper.find('.fixed').classes()).toContain('justify-end') }) it('aligns to the left when side is "left"', () => { const wrapper = mountComponent({ modelValue: true, side: 'left' }) expect(wrapper.find('.fixed').classes()).toContain('justify-start') }) it('emits update:modelValue false and close on backdrop click (dismissable)', async () => { const wrapper = mountComponent({ modelValue: true }) await wrapper.find('[data-test="backdrop"]').trigger('click') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([false]) expect(wrapper.emitted('close')).toHaveLength(1) }) it('does not close on backdrop click when dismissable is false', async () => { const wrapper = mountComponent({ modelValue: true, dismissable: false }) await wrapper.find('[data-test="backdrop"]').trigger('click') expect(wrapper.emitted('update:modelValue')).toBeUndefined() }) it('applies overlayClass to the backdrop', () => { const wrapper = mountComponent({ modelValue: true, overlayClass: 'bg-black/70' }) expect(wrapper.find('[data-test="backdrop"]').classes()).toContain('bg-black/70') }) it('closes on Escape key when closeOnEscape is true', async () => { const wrapper = mountComponent({ modelValue: true }) await wrapper.find('[data-test="panel"]').trigger('keydown', { key: 'Escape' }) expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([false]) expect(wrapper.emitted('close')).toHaveLength(1) }) it('does not close on Escape when closeOnEscape is false', async () => { const wrapper = mountComponent({ modelValue: true, closeOnEscape: false }) await wrapper.find('[data-test="panel"]').trigger('keydown', { key: 'Escape' }) expect(wrapper.emitted('update:modelValue')).toBeUndefined() }) })