9ff3e83c03
Release / release (push) Successful in 1m24s
| 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: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-authored-by: matthieu <matthieu@yuno.malio.fr> Reviewed-on: #61 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
341 lines
13 KiB
TypeScript
341 lines
13 KiB
TypeScript
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 TabList from './TabList.vue'
|
|
|
|
type Tab = {
|
|
key: string
|
|
label: string
|
|
icon?: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
type TabListProps = {
|
|
tabs: Tab[]
|
|
modelValue?: string
|
|
id?: string
|
|
maxVisibleTabs?: number
|
|
maxWidth?: number
|
|
}
|
|
|
|
const TabListForTest = TabList as DefineComponent<TabListProps>
|
|
|
|
const tabs: Tab[] = [
|
|
{key: 'home', label: 'Accueil', icon: 'mdi:home'},
|
|
{key: 'settings', label: 'Paramètres'},
|
|
{key: 'profile', label: 'Profil', icon: 'mdi:account'},
|
|
]
|
|
|
|
function mountComponent(props: TabListProps, slots?: Record<string, string>) {
|
|
return mount(TabListForTest, {
|
|
props,
|
|
slots,
|
|
})
|
|
}
|
|
|
|
describe('MalioTabList', () => {
|
|
it('renders all tab buttons with correct labels', () => {
|
|
const wrapper = mountComponent({tabs})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons).toHaveLength(3)
|
|
expect(buttons[0].text()).toContain('Accueil')
|
|
expect(buttons[1].text()).toContain('Paramètres')
|
|
expect(buttons[2].text()).toContain('Profil')
|
|
})
|
|
|
|
it('renders icons for tabs that have one', () => {
|
|
const wrapper = mountComponent({tabs})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons[0].find('svg').exists()).toBe(true)
|
|
expect(buttons[2].find('svg').exists()).toBe(true)
|
|
})
|
|
|
|
it('does not render icon when tab has no icon', () => {
|
|
const wrapper = mountComponent({tabs})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons[1].find('svg').exists()).toBe(false)
|
|
})
|
|
|
|
it('first tab is active by default in uncontrolled mode', () => {
|
|
const wrapper = mountComponent({tabs})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons[0].attributes('aria-selected')).toBe('true')
|
|
expect(buttons[1].attributes('aria-selected')).toBe('false')
|
|
expect(buttons[2].attributes('aria-selected')).toBe('false')
|
|
})
|
|
|
|
it('shows the panel content for the active tab (v-show)', () => {
|
|
const wrapper = mountComponent({tabs}, {
|
|
home: '<p>Home content</p>',
|
|
settings: '<p>Settings content</p>',
|
|
profile: '<p>Profile content</p>',
|
|
})
|
|
const panels = wrapper.findAll('[role="tabpanel"]')
|
|
expect(panels[0].attributes('style')).toBeUndefined()
|
|
expect(panels[1].attributes('style')).toContain('display: none')
|
|
expect(panels[2].attributes('style')).toContain('display: none')
|
|
})
|
|
|
|
it('switches tab on click in uncontrolled mode', async () => {
|
|
const wrapper = mountComponent({tabs})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
|
|
await buttons[1].trigger('click')
|
|
|
|
expect(buttons[1].attributes('aria-selected')).toBe('true')
|
|
expect(buttons[0].attributes('aria-selected')).toBe('false')
|
|
})
|
|
|
|
it('emits update:modelValue on click in controlled mode', async () => {
|
|
const wrapper = mountComponent({tabs, modelValue: 'home'})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
|
|
await buttons[2].trigger('click')
|
|
|
|
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['profile'])
|
|
})
|
|
|
|
it('respects modelValue for active tab in controlled mode', () => {
|
|
const wrapper = mountComponent({tabs, modelValue: 'settings'})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons[0].attributes('aria-selected')).toBe('false')
|
|
expect(buttons[1].attributes('aria-selected')).toBe('true')
|
|
expect(buttons[2].attributes('aria-selected')).toBe('false')
|
|
})
|
|
|
|
it('sets correct aria-controls and aria-labelledby', () => {
|
|
const wrapper = mountComponent({tabs, id: 'test'})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
const panels = wrapper.findAll('[role="tabpanel"]')
|
|
|
|
expect(buttons[0].attributes('aria-controls')).toBe('test-panel-home')
|
|
expect(buttons[1].attributes('aria-controls')).toBe('test-panel-settings')
|
|
expect(panels[0].attributes('aria-labelledby')).toBe('test-tab-home')
|
|
expect(panels[1].attributes('aria-labelledby')).toBe('test-tab-settings')
|
|
})
|
|
|
|
it('has role="tablist" on the tab container', () => {
|
|
const wrapper = mountComponent({tabs})
|
|
expect(wrapper.find('[role="tablist"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('active tab has tabindex 0, others have -1', () => {
|
|
const wrapper = mountComponent({tabs})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons[0].attributes('tabindex')).toBe('0')
|
|
expect(buttons[1].attributes('tabindex')).toBe('-1')
|
|
expect(buttons[2].attributes('tabindex')).toBe('-1')
|
|
})
|
|
|
|
it('renders icon props correctly via findComponent', () => {
|
|
const wrapper = mount(TabListForTest, {
|
|
props: {tabs},
|
|
})
|
|
const icons = wrapper.findAllComponents(IconifyIcon)
|
|
expect(icons).toHaveLength(2)
|
|
expect(icons[0].props('icon')).toBe('mdi:home')
|
|
expect(icons[1].props('icon')).toBe('mdi:account')
|
|
})
|
|
|
|
it('sets disabled attribute and aria-disabled on disabled tabs', () => {
|
|
const disabledTabs: Tab[] = [
|
|
{key: 'a', label: 'A'},
|
|
{key: 'b', label: 'B', disabled: true},
|
|
]
|
|
const wrapper = mountComponent({tabs: disabledTabs})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons[1].attributes('disabled')).toBeDefined()
|
|
expect(buttons[1].attributes('aria-disabled')).toBe('true')
|
|
})
|
|
|
|
it('applies cursor-not-allowed on disabled tabs', () => {
|
|
const disabledTabs: Tab[] = [
|
|
{key: 'a', label: 'A'},
|
|
{key: 'b', label: 'B', disabled: true},
|
|
]
|
|
const wrapper = mountComponent({tabs: disabledTabs})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons[1].classes()).toContain('cursor-not-allowed')
|
|
expect(buttons[1].classes()).not.toContain('hover:text-m-primary/70')
|
|
})
|
|
|
|
it('does not emit update:modelValue when clicking a disabled tab', async () => {
|
|
const disabledTabs: Tab[] = [
|
|
{key: 'a', label: 'A'},
|
|
{key: 'b', label: 'B', disabled: true},
|
|
]
|
|
const wrapper = mountComponent({tabs: disabledTabs, modelValue: 'a'})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
|
|
await buttons[1].trigger('click')
|
|
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
})
|
|
|
|
it('does not change active tab in uncontrolled mode when clicking disabled tab', async () => {
|
|
const disabledTabs: Tab[] = [
|
|
{key: 'a', label: 'A'},
|
|
{key: 'b', label: 'B', disabled: true},
|
|
]
|
|
const wrapper = mountComponent({tabs: disabledTabs})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
|
|
await buttons[1].trigger('click')
|
|
|
|
expect(buttons[0].attributes('aria-selected')).toBe('true')
|
|
expect(buttons[1].attributes('aria-selected')).toBe('false')
|
|
})
|
|
})
|
|
|
|
describe('MalioTabList — fenêtrage maxVisibleTabs', () => {
|
|
const sevenTabs: Tab[] = [
|
|
{key: 't1', label: 'Tab 1'},
|
|
{key: 't2', label: 'Tab 2'},
|
|
{key: 't3', label: 'Tab 3'},
|
|
{key: 't4', label: 'Tab 4'},
|
|
{key: 't5', label: 'Tab 5'},
|
|
{key: 't6', label: 'Tab 6'},
|
|
{key: 't7', label: 'Tab 7'},
|
|
]
|
|
|
|
it('applies the default maxWidth (1100px) on the tabs container when windowed', () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
|
expect(wrapper.find('[role="tablist"]').attributes('style')).toContain('max-width: 1100px')
|
|
})
|
|
|
|
it('applies a custom maxWidth on the tabs container', () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5, maxWidth: 1200})
|
|
expect(wrapper.find('[role="tablist"]').attributes('style')).toContain('max-width: 1200px')
|
|
})
|
|
|
|
it('renders only maxVisibleTabs buttons and disables prev at start', () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons).toHaveLength(5)
|
|
expect(buttons[0].text()).toContain('Tab 1')
|
|
expect(buttons[4].text()).toContain('Tab 5')
|
|
|
|
const prev = wrapper.find('[data-test="tab-prev"]')
|
|
const next = wrapper.find('[data-test="tab-next"]')
|
|
expect(prev.exists()).toBe(true)
|
|
expect(next.exists()).toBe(true)
|
|
expect(prev.attributes('disabled')).toBeDefined()
|
|
expect(next.attributes('disabled')).toBeUndefined()
|
|
})
|
|
|
|
it('shifts the window by 1 on next click', async () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
|
|
|
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
|
|
|
const labels = wrapper.findAll('[role="tab"]').map(b => b.text())
|
|
expect(labels.some(l => l.includes('Tab 1'))).toBe(false)
|
|
expect(labels.some(l => l.includes('Tab 6'))).toBe(true)
|
|
expect(labels).toHaveLength(5)
|
|
|
|
expect(wrapper.find('[data-test="tab-prev"]').attributes('disabled')).toBeUndefined()
|
|
})
|
|
|
|
it('disables next at the end and shows the last window', async () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
|
|
|
// 7 - 5 = 2 clicks to reach the end
|
|
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
|
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
|
|
|
const next = wrapper.find('[data-test="tab-next"]')
|
|
expect(next.attributes('disabled')).toBeDefined()
|
|
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons).toHaveLength(5)
|
|
// last window starts at tabs[length-5] = tabs[2] = Tab 3
|
|
expect(buttons[0].text()).toContain('Tab 3')
|
|
expect(buttons[4].text()).toContain('Tab 7')
|
|
})
|
|
|
|
it('clicking next past the end does not overshoot', async () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
|
const next = wrapper.find('[data-test="tab-next"]')
|
|
|
|
await next.trigger('click')
|
|
await next.trigger('click')
|
|
await next.trigger('click') // guarded, no-op
|
|
await next.trigger('click') // guarded, no-op
|
|
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
expect(buttons).toHaveLength(5)
|
|
expect(buttons[0].text()).toContain('Tab 3')
|
|
expect(buttons[4].text()).toContain('Tab 7')
|
|
})
|
|
|
|
it('renders no arrows and all tabs when maxVisibleTabs is undefined', () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs})
|
|
expect(wrapper.findAll('[role="tab"]')).toHaveLength(7)
|
|
expect(wrapper.find('[data-test="tab-prev"]').exists()).toBe(false)
|
|
expect(wrapper.find('[data-test="tab-next"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('renders no arrows when maxVisibleTabs >= tabs.length', () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 7})
|
|
expect(wrapper.findAll('[role="tab"]')).toHaveLength(7)
|
|
expect(wrapper.find('[data-test="tab-prev"]').exists()).toBe(false)
|
|
expect(wrapper.find('[data-test="tab-next"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('selecting a visible tab activates it without moving the window', async () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
|
const buttons = wrapper.findAll('[role="tab"]')
|
|
|
|
await buttons[2].trigger('click')
|
|
|
|
const after = wrapper.findAll('[role="tab"]')
|
|
expect(after[2].attributes('aria-selected')).toBe('true')
|
|
// window unchanged
|
|
expect(after[0].text()).toContain('Tab 1')
|
|
expect(after).toHaveLength(5)
|
|
})
|
|
|
|
it('keeps the active panel rendered even when its tab is outside the window', async () => {
|
|
const wrapper = mountComponent(
|
|
{tabs: sevenTabs, maxVisibleTabs: 5, modelValue: 't1'},
|
|
{t1: '<p>Panel 1</p>'},
|
|
)
|
|
|
|
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
|
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
|
|
|
// Tab 1 is no longer in the window
|
|
const labels = wrapper.findAll('[role="tab"]').map(b => b.text())
|
|
expect(labels.some(l => l.includes('Tab 1'))).toBe(false)
|
|
|
|
// but its panel is still rendered and visible
|
|
const panels = wrapper.findAll('[role="tabpanel"]')
|
|
expect(panels).toHaveLength(7)
|
|
expect(wrapper.text()).toContain('Panel 1')
|
|
})
|
|
|
|
it('keeps exactly one rendered tab with tabindex=0 when the active tab scrolls out of the window', async () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
|
|
|
// active tab is the first one (t1) by default; scroll it out of the window
|
|
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
|
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
|
|
|
// t1 is no longer rendered
|
|
const labels = wrapper.findAll('[role="tab"]').map(b => b.text())
|
|
expect(labels.some(l => l.includes('Tab 1'))).toBe(false)
|
|
|
|
const focusable = wrapper.findAll('[role="tab"]').filter(b => b.attributes('tabindex') === '0')
|
|
expect(focusable).toHaveLength(1)
|
|
// falls back to the first visible tab (Tab 3)
|
|
expect(focusable[0].text()).toContain('Tab 3')
|
|
})
|
|
|
|
it('arrows expose aria-labels', () => {
|
|
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
|
expect(wrapper.find('[data-test="tab-prev"]').attributes('aria-label')).toBe('Onglets précédents')
|
|
expect(wrapper.find('[data-test="tab-next"]').attributes('aria-label')).toBe('Onglets suivants')
|
|
})
|
|
})
|