import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import { nextTick } from 'vue' import SyTabs from '@/components/Customs/SyTabs/SyTabs.vue' // Mock RouterLink component const RouterLink = { name: 'RouterLink', props: ['to'], template: '', } describe('SyTabs', () => { // Données de test const testItems = [ { label: 'Tab 1', value: 'tab1', content: 'Contenu du Tab 1' }, { label: 'Tab 2', value: 'tab2', content: 'Contenu du Tab 2' }, { label: 'Tab 3', value: 'tab3', content: 'Contenu du Tab 3' }, ] // Options de montage par défaut const defaultMountOptions = { props: { items: testItems, }, global: { // Mock vue-router and provide RouterLink component components: { RouterLink, }, // Mock $router used in the component mocks: { $router: { push: vi.fn(), replace: vi.fn(), }, }, }, } // Utilitaire pour créer le wrapper avec les options par défaut const createWrapper = (options = {}) => { return mount(SyTabs, { ...defaultMountOptions, ...options, }) } // Tests de rendu describe('Rendu', () => { it('doit afficher correctement les onglets', () => { const wrapper = createWrapper() // Vérifier que tous les onglets sont rendus const tabButtons = wrapper.findAll('.sy-tabs__button') expect(tabButtons.length).toBe(testItems.length) // Vérifier le texte des onglets testItems.forEach((item, index) => { expect(tabButtons[index]?.text()).toBe(item.label.toUpperCase()) }) }) it('doit afficher le contenu de l\'onglet actif', () => { const wrapper = createWrapper() // Par défaut, le premier onglet devrait être actif const visiblePanel = wrapper.find('.sy-tabs-panel:not([hidden])') expect(visiblePanel.exists()).toBe(true) expect(visiblePanel.text()).toContain(testItems[0]?.content) }) it('doit ajouter la classe active au bouton d\'onglet actif', () => { const wrapper = createWrapper() // Le premier onglet devrait avoir la classe active const activeTab = wrapper.find('.sy-tabs__button--active') expect(activeTab.exists()).toBe(true) expect(activeTab.text()).toBe(testItems[0]?.label.toUpperCase()) }) }) // Tests des props describe('Props', () => { it('doit respecter la prop modelValue numérique', async () => { const wrapper = createWrapper({ props: { ...defaultMountOptions.props, modelValue: 1, }, }) await nextTick() // Vérifier que le deuxième onglet est actif const activeTab = wrapper.find('.sy-tabs__button--active') expect(activeTab.text()).toBe(testItems[1]?.label.toUpperCase()) // Vérifier que le bon panneau est affiché const visiblePanel = wrapper.find('.sy-tabs-panel:not([hidden])') expect(visiblePanel.attributes('id')).toBe('panel-1') }) it('doit respecter la prop modelValue de type string', async () => { const wrapper = createWrapper({ props: { ...defaultMountOptions.props, modelValue: 'tab2', }, }) await nextTick() // Vérifier que le bon onglet est actif const activeTab = wrapper.find('.sy-tabs__button--active') expect(activeTab.text()).toBe(testItems[1]?.label.toUpperCase()) }) }) // Tests d'interaction describe('Interaction', () => { it('doit changer d\'onglet actif au clic', async () => { const wrapper = createWrapper() // Cliquer sur le deuxième onglet const secondTab = wrapper.findAll('.sy-tabs__button')[1]! await secondTab.trigger('click') // Vérifier que le deuxième onglet est maintenant actif expect(secondTab.classes()).toContain('sy-tabs__button--active') // Vérifier que l'événement update:modelValue a été émis const emitted = wrapper.emitted('update:modelValue') expect(emitted).toBeTruthy() if (emitted) { expect(emitted[0]).toEqual([1]) } // Vérifier que le bon panneau est visible const visiblePanel = wrapper.find('.sy-tabs-panel:not([hidden])') expect(visiblePanel.attributes('id')).toBe('panel-1') }) it('doit émettre l\'événement update:modelValue avec la bonne valeur', async () => { const wrapper = createWrapper({ props: { ...defaultMountOptions.props, modelValue: 'tab1', }, }) // Cliquer sur le troisième onglet const thirdTab = wrapper.findAll('.sy-tabs__button')[2]! await thirdTab.trigger('click') // Vérifier que l'événement update:modelValue a été émis avec la bonne valeur const emitted = wrapper.emitted('update:modelValue') expect(emitted).toBeTruthy() if (emitted) { expect(emitted[0]).toEqual(['tab3']) } }) }) // Tests du mode Navigation describe('Mode Navigation', () => { const navItems = [ { label: 'Nav 1', value: 'nav1', to: '/path-1' }, { label: 'Nav 2', value: 'nav2', href: 'https://example.com' }, { label: 'Nav 3', value: 'nav3', to: '/path-3', disabled: true }, ] it('doit utiliser la sémantique de navigation', () => { const wrapper = createWrapper({ props: { ...defaultMountOptions.props, items: navItems, }, }) // Vérifier les attributs du conteneur const nav = wrapper.find('nav[role="navigation"]') expect(nav.exists()).toBe(true) expect(nav.attributes('aria-label')).toBeDefined() // Vérifier les attributs des liens const buttons = wrapper.findAll('.sy-tabs__button') expect(buttons[0]!.attributes('role')).toBeUndefined() expect(buttons[0]!.attributes('aria-current')).toBe('page') expect(buttons[0]!.attributes('aria-selected')).toBeUndefined() expect(buttons[0]!.attributes('aria-controls')).toBeUndefined() expect(buttons[1]!.attributes('aria-current')).toBeUndefined() }) it('ne doit pas générer les rôles tabpanel pour les panneaux', () => { const wrapper = createWrapper({ props: { ...defaultMountOptions.props, items: navItems, }, }) const panel = wrapper.find('.sy-tabs-panel') expect(panel.attributes('role')).toBeUndefined() expect(panel.attributes('aria-labelledby')).toBeUndefined() }) }) // Tests d'accessibilité et navigation clavier describe('Accessibilité et navigation clavier', () => { it('doit contenir les attributs ARIA appropriés', () => { const wrapper = createWrapper() // Vérifier les attributs ARIA du tablist const nav = wrapper.find('[role="tablist"]') expect(nav.exists()).toBe(true) expect(nav.attributes('aria-label')).toBeDefined() // Vérifier les attributs ARIA des onglets const firstTab = wrapper.findAll('[role="tab"]')[0]! expect(firstTab.attributes('aria-selected')).toBe('true') expect(firstTab.attributes('aria-controls')).toBe('panel-0') // Vérifier les attributs ARIA des panneaux const firstPanel = wrapper.find('[role="tabpanel"]') expect(firstPanel.attributes('aria-labelledby')).toBe('tab-0') }) it('doit activer un onglet avec les touches Enter/Space', async () => { const wrapper = createWrapper() // Simuler une pression de touche Enter sur le deuxième onglet const secondTab = wrapper.findAll('.sy-tabs__button')[1]! await secondTab.trigger('keydown', { key: 'Enter', }) // Vérifier que le deuxième onglet est actif expect(secondTab.classes()).toContain('sy-tabs__button--active') }) it('doit naviguer vers l\'onglet de gauche avec ArrowLeft', async () => { // Mock pour document.getElementById const mockElement = { focus: vi.fn(), } document.getElementById = vi.fn().mockImplementation((id) => { if (id === 'tab-0') { return mockElement } return null }) const wrapper = createWrapper({ props: { ...defaultMountOptions.props, modelValue: 1, // Commencer au deuxième onglet }, }) // Accéder directement à l'instance du composant const vm = wrapper.vm as unknown as { handleArrowNavigation: (event: KeyboardEvent, activeIndex: number) => void } // Créer un événement avec preventDefault const event = new KeyboardEvent('keydown', { key: 'ArrowLeft' }) event.preventDefault = vi.fn() // Appeler directement la méthode handleArrowNavigation vm.handleArrowNavigation(event, 1) // 1 = deuxième onglet (index actif) // Vérifier que l'événement a été émis avec la bonne valeur const emitted = wrapper.emitted('update:modelValue') expect(emitted).toBeTruthy() // Type guard pour TypeScript if (emitted) { expect(emitted[0]).toEqual([0]) } }) it('doit naviguer vers l\'onglet de droite avec ArrowRight', async () => { // Mock pour document.getElementById qui filtre les appels non pertinents const mockElement = { focus: vi.fn(), } // Mock complet qui retourne mockElement uniquement pour tab-1 document.getElementById = vi.fn().mockImplementation((id) => { if (id === 'tab-1') { return mockElement } return null }) const wrapper = createWrapper() // Simuler une navigation avec flèche droite const activeTab = wrapper.find('.sy-tabs__button--active') await activeTab.trigger('keydown', { key: 'ArrowRight', preventDefault: vi.fn(), }) // Vérifier l'émission de l'événement const emitted = wrapper.emitted('update:modelValue') expect(emitted).toBeTruthy() if (emitted) { expect(emitted[0]).toEqual([1]) } }) it('doit naviguer vers le premier onglet avec Home', async () => { // Mock pour document.getElementById qui filtre les appels non pertinents const mockElement = { focus: vi.fn(), } // Mock complet qui retourne mockElement uniquement pour tab-0 document.getElementById = vi.fn().mockImplementation((id) => { if (id === 'tab-0') { return mockElement } return null }) const wrapper = createWrapper({ props: { ...defaultMountOptions.props, modelValue: 2, // Commencer au dernier onglet }, }) // Simuler une navigation avec Home const activeTab = wrapper.find('.sy-tabs__button--active') await activeTab.trigger('keydown', { key: 'Home', preventDefault: vi.fn(), }) // Vérifier l'émission de l'événement const emitted = wrapper.emitted('update:modelValue') expect(emitted).toBeTruthy() if (emitted) { expect(emitted[0]).toEqual([0]) } }) it('doit naviguer vers le dernier onglet avec End', async () => { // Mock pour document.getElementById qui filtre les appels non pertinents const mockElement = { focus: vi.fn(), } // Mock complet qui retourne mockElement uniquement pour tab-2 document.getElementById = vi.fn().mockImplementation((id) => { if (id === 'tab-2') { return mockElement } return null }) const wrapper = createWrapper() // Simuler une navigation avec End const activeTab = wrapper.find('.sy-tabs__button--active') await activeTab.trigger('keydown', { key: 'End', preventDefault: vi.fn(), }) // Vérifier l'émission de l'événement const emitted = wrapper.emitted('update:modelValue') expect(emitted).toBeTruthy() if (emitted) { expect(emitted[0]).toEqual([2]) } }) }) // Tests de la boucle de navigation describe('Boucle de navigation', () => { it('doit boucler vers le dernier onglet avec ArrowLeft depuis le premier', async () => { // Mock pour document.getElementById qui filtre les appels non pertinents const mockElement = { focus: vi.fn(), } // Mock complet qui retourne mockElement uniquement pour le dernier tab const lastTabIndex = testItems.length - 1 document.getElementById = vi.fn().mockImplementation((id) => { if (id === `tab-${lastTabIndex}`) { return mockElement } return null }) const wrapper = createWrapper() // Simuler une navigation avec flèche gauche depuis le premier onglet const firstTab = wrapper.findAll('.sy-tabs__button')[0]! await firstTab.trigger('keydown', { key: 'ArrowLeft', preventDefault: vi.fn(), }) // Vérifier l'émission de l'événement const emitted = wrapper.emitted('update:modelValue') expect(emitted).toBeTruthy() if (emitted) { expect(emitted[0]).toEqual([lastTabIndex]) } }) it('doit boucler vers le premier onglet avec ArrowRight depuis le dernier', async () => { // Mock pour document.getElementById const mockElement = { focus: vi.fn(), } document.getElementById = vi.fn().mockImplementation((id) => { if (id === 'tab-0') { return mockElement } return null }) const lastIndex = testItems.length - 1 const wrapper = createWrapper({ props: { ...defaultMountOptions.props, modelValue: lastIndex, }, }) // Accéder directement à l'instance du composant const vm = wrapper.vm as unknown as { handleArrowNavigation: (event: KeyboardEvent, activeIndex: number) => void } // Créer un événement avec preventDefault const event = new KeyboardEvent('keydown', { key: 'ArrowRight' }) event.preventDefault = vi.fn() // Appeler directement la méthode handleArrowNavigation vm.handleArrowNavigation(event, lastIndex) // dernier onglet // Vérifier l'émission de l'événement const emitted = wrapper.emitted('update:modelValue') expect(emitted).toBeTruthy() if (emitted) { expect(emitted[0]).toEqual([0]) } }) }) // Tests items désactivés avec navigation (branches template lignes 444-504) describe('Items désactivés en mode navigation', () => { it('rend un