UNPKG

@mozaic-ds/vue

Version:

Mozaic-Vue is the Vue.js implementation of ADEO Design system

233 lines (198 loc) 8.23 kB
import { h } from 'vue'; import { mount } from '@vue/test-utils'; import { describe, it, expect } from 'vitest'; import MPopover from './MPopover.vue'; import MIconButton from '../iconbutton/MIconButton.vue'; describe('MPopover.vue', () => { it('applies classes based on props', () => { const wrapper = mount(MPopover, { props: { appearance: 'inverse', position: 'right', pointer: false, size: 'l', }, }); const rootDiv = wrapper.find('.mc-popover'); expect(rootDiv.exists()).toBe(true); expect(rootDiv.classes()).toContain('mc-popover'); expect(rootDiv.classes()).toContain('mc-popover--inverse'); expect(rootDiv.classes()).toContain('mc-popover--right'); expect(rootDiv.classes()).toContain('mc-popover--no-pointer'); expect(rootDiv.classes()).toContain('mc-popover--l'); }); it('sets aria-labelledby and aria-describedby when title/description provided', () => { const wrapper = mount(MPopover, { props: { title: 'My title', description: 'My description' }, }); const wrapperEl = wrapper.find('.mc-popover__wrapper'); const id = wrapperEl.attributes('id'); expect(id).toBeTruthy(); expect(wrapperEl.attributes('aria-labelledby')).toBe(`${id}-title`); expect(wrapperEl.attributes('aria-describedby')).toBe(`${id}-description`); }); it('renders title and description', () => { const wrapper = mount(MPopover, { props: { title: 'T', description: 'D' }, }); const wrapperEl = wrapper.find('.mc-popover__wrapper'); const id = wrapperEl.attributes('id'); const title = wrapper.find(`#${id}-title`); const description = wrapper.find(`#${id}-description`); expect(title.exists()).toBe(true); expect(title.text()).toBe('T'); expect(description.exists()).toBe(true); expect(description.text()).toBe('D'); }); it('renders default slot content', () => { const wrapper = mount(MPopover, { slots: { default: '<div class="slot-content">Hello</div>' }, }); expect(wrapper.find('.slot-content').exists()).toBe(true); expect(wrapper.find('.slot-content').text()).toBe('Hello'); }); it('renders footer slot when provided', () => { const wrapper = mount(MPopover, { slots: { footer: '<button class="footer-button">OK</button>' }, }); const footer = wrapper.find('.mc-popover__footer'); expect(footer.exists()).toBe(true); const button = footer.find('button'); expect(button.exists()).toBe(true); expect(button.text()).toBe('OK'); }); it('renders close button when closable is true and binds id', () => { const wrapper = mount(MPopover, { props: { closable: true } }); const closeButton = wrapper.findComponent(MIconButton); expect(closeButton.exists()).toBe(true); const wrapperEl = wrapper.find('.mc-popover__wrapper'); const id = wrapperEl.attributes('id'); expect(closeButton.attributes('popovertarget')).toBe(id); expect(closeButton.attributes('aria-label')).toBe('Close button'); }); it('activator slot receives id and can bind popovertarget', () => { const wrapper = mount(MPopover, { slots: { activator: (slotProps: { id: string }) => h( 'button', { popovertarget: slotProps.id, class: 'activator' }, 'Open', ), }, }); const wrapperEl = wrapper.find('.mc-popover__wrapper'); const id = wrapperEl.attributes('id'); const activator = wrapper.find('.activator'); expect(activator.exists()).toBe(true); expect(activator.attributes('popovertarget')).toBe(id); }); describe('Focus management (toggle)', () => { it('focuses the first focusable element inside when opened', async () => { const wrapper = mount(MPopover, { attachTo: document.body, props: { closable: true }, }); const popoverEl = wrapper.find('.mc-popover__wrapper').element; const toggleEvent = new Event('toggle') as ToggleEvent; Object.defineProperty(toggleEvent, 'newState', { value: 'open' }); popoverEl.dispatchEvent(toggleEvent); await wrapper.vm.$nextTick(); const firstFocusable = popoverEl.querySelector<HTMLElement>( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ); expect(document.activeElement).toBe(firstFocusable); wrapper.unmount(); }); it('focuses the popover container when opened without focusable content', async () => { const wrapper = mount(MPopover, { attachTo: document.body, props: { closable: false, title: 'Information', description: 'Read-only content', }, }); const popoverEl = wrapper.find('.mc-popover__wrapper') .element as HTMLElement; const toggleEvent = new Event('toggle') as ToggleEvent; Object.defineProperty(toggleEvent, 'newState', { value: 'open' }); popoverEl.dispatchEvent(toggleEvent); await wrapper.vm.$nextTick(); expect(document.activeElement).toBe(popoverEl); wrapper.unmount(); }); it('returns focus to the trigger when closed with focus inside the popover', async () => { const wrapper = mount(MPopover, { attachTo: document.body, props: { closable: true }, slots: { activator: (slotProps: { id: string }) => h('button', { popovertarget: slotProps.id, class: 'trigger-btn', }), }, }); // Simulate focus being on the close button (inside the popover) const closeBtn = wrapper.findComponent(MIconButton).find('button') .element as HTMLElement; closeBtn.focus(); const popoverEl = wrapper.find('.mc-popover__wrapper').element; const toggleEvent = new Event('toggle') as ToggleEvent; Object.defineProperty(toggleEvent, 'newState', { value: 'closed' }); popoverEl.dispatchEvent(toggleEvent); await wrapper.vm.$nextTick(); const trigger = wrapper.find('.trigger-btn').element as HTMLElement; expect(document.activeElement).toBe(trigger); wrapper.unmount(); }); it('returns focus to the trigger when closed with focus on document.body', async () => { const wrapper = mount(MPopover, { attachTo: document.body, slots: { activator: (slotProps: { id: string }) => h('button', { popovertarget: slotProps.id, class: 'trigger-btn', }), }, }); // Simulate browser moving focus to body after popover collapses (document.activeElement as HTMLElement)?.blur?.(); const popoverEl = wrapper.find('.mc-popover__wrapper').element; const toggleEvent = new Event('toggle') as ToggleEvent; Object.defineProperty(toggleEvent, 'newState', { value: 'closed' }); popoverEl.dispatchEvent(toggleEvent); await wrapper.vm.$nextTick(); const trigger = wrapper.find('.trigger-btn').element as HTMLElement; expect(document.activeElement).toBe(trigger); wrapper.unmount(); }); it('does not steal focus when closed by an outside click (focus already on another element)', async () => { const wrapper = mount(MPopover, { attachTo: document.body, slots: { activator: (slotProps: { id: string }) => h('button', { popovertarget: slotProps.id, class: 'trigger-btn', }), }, }); // Simulate user clicking an outside input const outsideInput = document.createElement('input'); document.body.appendChild(outsideInput); outsideInput.focus(); const popoverEl = wrapper.find('.mc-popover__wrapper').element; const toggleEvent = new Event('toggle') as ToggleEvent; Object.defineProperty(toggleEvent, 'newState', { value: 'closed' }); popoverEl.dispatchEvent(toggleEvent); await wrapper.vm.$nextTick(); // Focus should remain on the outside input, not be stolen by the trigger expect(document.activeElement).toBe(outsideInput); outsideInput.remove(); wrapper.unmount(); }); }); });