import {describe, expect, it} from 'vitest' import {mount} from '@vue/test-utils' import {nextTick} from 'vue' import Accordion from './Accordion.vue' import AccordionItem from './AccordionItem.vue' const TWO_ITEMS = `

Contenu prix

Contenu catégorie

` function mountAccordion(props: Record = {}, slot: string = TWO_ITEMS, attachTo?: HTMLElement) { return mount(Accordion, { props, slots: {default: slot}, attachTo, global: {components: {MalioAccordionItem: AccordionItem}}, }) } describe('MalioAccordion — rendu & mode multiple', () => { it('renders each item header with its title', () => { const wrapper = mountAccordion() const headers = wrapper.findAll('button[aria-expanded]') expect(headers).toHaveLength(2) expect(headers[0].text()).toContain('Prix') expect(headers[1].text()).toContain('Catégorie') }) it('renders the slot content of each panel', () => { const wrapper = mountAccordion() expect(wrapper.html()).toContain('Contenu prix') expect(wrapper.html()).toContain('Contenu catégorie') }) it('all panels are collapsed by default', () => { const wrapper = mountAccordion() const headers = wrapper.findAll('button[aria-expanded]') expect(headers[0].attributes('aria-expanded')).toBe('false') expect(headers[1].attributes('aria-expanded')).toBe('false') const regions = wrapper.findAll('[role="region"]') expect(regions[0].classes()).toContain('grid-rows-[0fr]') }) it('opens a panel on header click (multiple mode is default)', async () => { const wrapper = mountAccordion() const headers = wrapper.findAll('button[aria-expanded]') await headers[0].trigger('click') expect(headers[0].attributes('aria-expanded')).toBe('true') const regions = wrapper.findAll('[role="region"]') expect(regions[0].classes()).toContain('grid-rows-[1fr]') }) it('keeps multiple panels open simultaneously in multiple mode', async () => { const wrapper = mountAccordion() const headers = wrapper.findAll('button[aria-expanded]') await headers[0].trigger('click') await headers[1].trigger('click') expect(headers[0].attributes('aria-expanded')).toBe('true') expect(headers[1].attributes('aria-expanded')).toBe('true') }) it('closes an open panel when its header is clicked again', async () => { const wrapper = mountAccordion() const headers = wrapper.findAll('button[aria-expanded]') await headers[0].trigger('click') await headers[0].trigger('click') expect(headers[0].attributes('aria-expanded')).toBe('false') }) it('wires aria-controls / aria-labelledby / role=region correctly', () => { const wrapper = mountAccordion({id: 'acc'}) const headers = wrapper.findAll('button[aria-expanded]') const regions = wrapper.findAll('[role="region"]') expect(headers[0].attributes('id')).toBe('acc-header-prix') expect(headers[0].attributes('aria-controls')).toBe('acc-panel-prix') expect(regions[0].attributes('id')).toBe('acc-panel-prix') expect(regions[0].attributes('aria-labelledby')).toBe('acc-header-prix') }) it('emits update:modelValue with an array in multiple mode', async () => { const wrapper = mountAccordion() const headers = wrapper.findAll('button[aria-expanded]') await headers[0].trigger('click') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['prix']]) await nextTick() }) }) describe('MalioAccordion — mode single & contrôlé', () => { it('opening a panel closes the others in single mode', async () => { const wrapper = mountAccordion({mode: 'single'}) const headers = wrapper.findAll('button[aria-expanded]') await headers[0].trigger('click') await headers[1].trigger('click') expect(headers[0].attributes('aria-expanded')).toBe('false') expect(headers[1].attributes('aria-expanded')).toBe('true') }) it('emits a string in single mode', async () => { const wrapper = mountAccordion({mode: 'single'}) const headers = wrapper.findAll('button[aria-expanded]') await headers[1].trigger('click') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['cat']) }) it('emits empty string when closing the open panel in single mode', async () => { const wrapper = mountAccordion({mode: 'single', modelValue: 'prix'}) const headers = wrapper.findAll('button[aria-expanded]') await headers[0].trigger('click') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['']) }) it('respects modelValue array in controlled multiple mode', () => { const wrapper = mountAccordion({modelValue: ['cat']}) const headers = wrapper.findAll('button[aria-expanded]') expect(headers[0].attributes('aria-expanded')).toBe('false') expect(headers[1].attributes('aria-expanded')).toBe('true') }) it('respects modelValue string in controlled single mode', () => { const wrapper = mountAccordion({mode: 'single', modelValue: 'prix'}) const headers = wrapper.findAll('button[aria-expanded]') expect(headers[0].attributes('aria-expanded')).toBe('true') expect(headers[1].attributes('aria-expanded')).toBe('false') }) it('does not mutate local state in controlled mode (emits only)', async () => { const wrapper = mountAccordion({modelValue: []}) const headers = wrapper.findAll('button[aria-expanded]') await headers[0].trigger('click') // état piloté par le parent : sans mise à jour de la prop, reste fermé expect(headers[0].attributes('aria-expanded')).toBe('false') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['prix']]) }) }) describe('MalioAccordion — defaultOpen, disabled & clavier', () => { const WITH_DEFAULT_OPEN = `

P

C

` const WITH_DISABLED = `

P

C

` it('opens defaultOpen items initially in uncontrolled mode', async () => { const wrapper = mountAccordion({}, WITH_DEFAULT_OPEN) await nextTick() const headers = wrapper.findAll('button[aria-expanded]') expect(headers[0].attributes('aria-expanded')).toBe('false') expect(headers[1].attributes('aria-expanded')).toBe('true') }) it('sets disabled and aria-disabled on a disabled item', () => { const wrapper = mountAccordion({}, WITH_DISABLED) const headers = wrapper.findAll('button[aria-expanded]') expect(headers[1].attributes('disabled')).toBeDefined() expect(headers[1].attributes('aria-disabled')).toBe('true') }) it('does not toggle a disabled item on click', async () => { const wrapper = mountAccordion({}, WITH_DISABLED) const headers = wrapper.findAll('button[aria-expanded]') await headers[1].trigger('click') expect(headers[1].attributes('aria-expanded')).toBe('false') expect(wrapper.emitted('update:modelValue')).toBeUndefined() }) it('moves focus to the next header on ArrowDown', async () => { const root = document.createElement('div') document.body.appendChild(root) const wrapper = mountAccordion({}, TWO_ITEMS, root) const headers = wrapper.findAll('button[aria-expanded]') ;(headers[0].element as HTMLElement).focus() await headers[0].trigger('keydown', {key: 'ArrowDown'}) expect(document.activeElement).toBe(headers[1].element) wrapper.unmount() root.remove() }) it('wraps focus to the first header on ArrowDown from the last', async () => { const root = document.createElement('div') document.body.appendChild(root) const wrapper = mountAccordion({}, TWO_ITEMS, root) const headers = wrapper.findAll('button[aria-expanded]') ;(headers[1].element as HTMLElement).focus() await headers[1].trigger('keydown', {key: 'ArrowDown'}) expect(document.activeElement).toBe(headers[0].element) wrapper.unmount() root.remove() }) it('moves focus to the previous header on ArrowUp', async () => { const root = document.createElement('div') document.body.appendChild(root) const wrapper = mountAccordion({}, TWO_ITEMS, root) const headers = wrapper.findAll('button[aria-expanded]') ;(headers[1].element as HTMLElement).focus() await headers[1].trigger('keydown', {key: 'ArrowUp'}) expect(document.activeElement).toBe(headers[0].element) wrapper.unmount() root.remove() }) it('skips disabled headers during keyboard navigation', async () => { const root = document.createElement('div') document.body.appendChild(root) const slot = `

A

B

C

` const wrapper = mountAccordion({}, slot, root) const headers = wrapper.findAll('button[aria-expanded]') ;(headers[0].element as HTMLElement).focus() await headers[0].trigger('keydown', {key: 'ArrowDown'}) // saute le header désactivé (B) pour aller directement à C expect(document.activeElement).toBe(headers[2].element) wrapper.unmount() root.remove() }) }) describe('MalioAccordion — overflow du panneau (popovers enfants)', () => { const ONE = `

contenu

` const ONE_OPEN = `

contenu

` it('clips the panel (overflow-hidden) while collapsed', () => { const wrapper = mountAccordion({}, ONE) const inner = wrapper.find('[role="region"] > div') expect(inner.classes()).toContain('overflow-hidden') expect(inner.classes()).not.toContain('overflow-visible') }) it('lets the panel overflow once open at mount (defaultOpen)', async () => { const wrapper = mountAccordion({}, ONE_OPEN) await nextTick() expect(wrapper.find('[role="region"] > div').classes()).toContain('overflow-visible') }) it('switches to overflow-visible after the open transition ends', async () => { const wrapper = mountAccordion({}, ONE) await wrapper.find('button[aria-expanded]').trigger('click') await wrapper.find('[role="region"]').trigger('transitionend', {propertyName: 'grid-template-rows'}) expect(wrapper.find('[role="region"] > div').classes()).toContain('overflow-visible') }) it('re-clips (overflow-hidden) as soon as it closes', async () => { const wrapper = mountAccordion({}, ONE_OPEN) await nextTick() await wrapper.find('button[aria-expanded]').trigger('click') expect(wrapper.find('[role="region"] > div').classes()).toContain('overflow-hidden') }) })