6efb830ffe
| 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é Reviewed-on: #54 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
257 lines
11 KiB
TypeScript
257 lines
11 KiB
TypeScript
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 = `
|
|
<MalioAccordionItem title="Prix" value="prix"><p>Contenu prix</p></MalioAccordionItem>
|
|
<MalioAccordionItem title="Catégorie" value="cat"><p>Contenu catégorie</p></MalioAccordionItem>
|
|
`
|
|
|
|
function mountAccordion(props: Record<string, unknown> = {}, 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 = `
|
|
<MalioAccordionItem title="Prix" value="prix"><p>P</p></MalioAccordionItem>
|
|
<MalioAccordionItem title="Catégorie" value="cat" :default-open="true"><p>C</p></MalioAccordionItem>
|
|
`
|
|
const WITH_DISABLED = `
|
|
<MalioAccordionItem title="Prix" value="prix"><p>P</p></MalioAccordionItem>
|
|
<MalioAccordionItem title="Catégorie" value="cat" :disabled="true"><p>C</p></MalioAccordionItem>
|
|
`
|
|
|
|
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 = `
|
|
<MalioAccordionItem title="A" value="a"><p>A</p></MalioAccordionItem>
|
|
<MalioAccordionItem title="B" value="b" :disabled="true"><p>B</p></MalioAccordionItem>
|
|
<MalioAccordionItem title="C" value="c"><p>C</p></MalioAccordionItem>
|
|
`
|
|
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 = `<MalioAccordionItem title="A" value="a"><p>contenu</p></MalioAccordionItem>`
|
|
const ONE_OPEN = `<MalioAccordionItem title="A" value="a" :default-open="true"><p>contenu</p></MalioAccordionItem>`
|
|
|
|
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')
|
|
})
|
|
})
|