@mozaic-ds/vue
Version:
Mozaic-Vue is the Vue.js implementation of ADEO Design system
233 lines (198 loc) • 8.23 kB
text/typescript
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();
});
});
});