@mozaic-ds/vue
Version:
Mozaic-Vue is the Vue.js implementation of ADEO Design system
252 lines (219 loc) • 8.24 kB
text/typescript
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { defineComponent, h, markRaw } from 'vue';
import MActionListbox from './MActionListbox.vue';
import MDivider from '../divider/MDivider.vue';
import MIconButton from '../iconbutton/MIconButton.vue';
import { Cross24 } from '@mozaic-ds/icons-vue';
// Dummy icon wrapped with markRaw to avoid reactive warnings
const DummyIcon = markRaw(
defineComponent({
name: 'DummyIcon',
render() {
return h('svg', { 'data-testid': 'icon' }, [
h('circle', { cx: 10, cy: 10, r: 10 }),
]);
},
}),
);
const items = [
{
label: 'Duplicate',
icon: DummyIcon,
},
{
id: 'move',
label: 'Move to...',
icon: DummyIcon,
},
{
label: 'Download',
icon: DummyIcon,
},
{
label: 'Delete',
icon: DummyIcon,
appearance: 'danger' as const,
divider: true,
},
];
describe('MActionListbox', () => {
function mountComponent(props = {}, options = {}) {
return mount(MActionListbox, {
props: { items, ...props },
global: {
components: {
MDivider,
MIconButton,
Cross24,
},
},
...options,
});
}
it('renders the title if provided', () => {
const wrapper = mountComponent({ title: 'Action List' });
expect(wrapper.find('.mc-listbox__title').text()).toBe('Action List');
});
it('renders all item labels', () => {
const wrapper = mountComponent();
items.forEach((item) => {
expect(wrapper.text()).toContain(item.label);
});
});
it('renders icons for each item', () => {
const wrapper = mountComponent();
const icons = wrapper.findAll('[data-testid="icon"]');
expect(icons.length).toBe(items.length);
});
it('adds danger class to items with appearance set to danger', () => {
const wrapper = mountComponent();
const dangerousItem = wrapper.findAll('.mc-action-list__element--danger');
expect(dangerousItem.length).toBe(1);
expect(dangerousItem[0].text()).toContain('Delete');
});
it('renders MDivider when divider is true', () => {
const wrapper = mountComponent();
const dividers = wrapper.findAll('.mc-action-list__divider');
expect(dividers.length).toBe(1);
});
it('applies disabled class when item.disabled is true', () => {
const disabledItems = [...items, { label: 'Disabled', disabled: true }];
const wrapper = mountComponent({ items: disabledItems });
const disabled = wrapper.findAll('.mc-action-list__element--disabled');
expect(disabled.length).toBe(1);
expect(disabled[0].text()).toContain('Disabled');
});
it('applies mc-listbox--bottom by default', () => {
const wrapper = mountComponent();
expect(wrapper.find('.mc-listbox').classes()).toContain(
'mc-listbox--bottom',
);
});
it('applies mc-listbox--top when position is "top"', () => {
const wrapper = mountComponent({ position: 'top' });
expect(wrapper.find('.mc-listbox').classes()).toContain('mc-listbox--top');
});
it('applies mc-listbox--left when position is "left"', () => {
const wrapper = mountComponent({ position: 'left' });
expect(wrapper.find('.mc-listbox').classes()).toContain('mc-listbox--left');
});
it('emits "close" when close button is clicked', async () => {
const wrapper = mountComponent({ title: 'Action List' });
await wrapper.find('.mc-listbox__close').trigger('click');
expect(wrapper.emitted('close')).toBeTruthy();
expect(wrapper.emitted('close')?.length).toBe(1);
});
it('emits "action" when an item is clicked', async () => {
const wrapper = mountComponent({ title: 'Action List' });
const actions = wrapper.findAll('.mc-action-list__button');
await actions[0].trigger('click');
expect(wrapper.emitted('action')).toBeTruthy();
expect(wrapper.emitted('action')?.[0][0]).toBe(0);
await actions[1].trigger('click');
expect(wrapper.emitted('action')?.[1][0]).toBe('move');
});
it('has role="menu" on the list and role="menuitem" on each button', () => {
const wrapper = mountComponent();
expect(wrapper.find('ul.mc-action-list').attributes('role')).toBe('menu');
const menuItems = wrapper.findAll('button[role="menuitem"]');
expect(menuItems.length).toBe(items.length);
});
describe('keyboard navigation', () => {
function mountAttached(props = {}) {
const div = document.createElement('div');
document.body.appendChild(div);
const wrapper = mount(MActionListbox, {
props: { items, ...props },
attachTo: div,
global: { components: { MDivider, MIconButton, Cross24 } },
});
return wrapper;
}
it('ArrowDown moves focus to the next item', async () => {
const wrapper = mountAttached();
const buttons = wrapper.findAll('button[role="menuitem"]');
await buttons[0].element.focus();
await wrapper
.find('ul[role="menu"]')
.trigger('keydown', { key: 'ArrowDown' });
expect(document.activeElement).toBe(buttons[1].element);
wrapper.unmount();
});
it('ArrowDown wraps from last to first item', async () => {
const wrapper = mountAttached();
const buttons = wrapper.findAll('button[role="menuitem"]');
await buttons[buttons.length - 1].element.focus();
await wrapper
.find('ul[role="menu"]')
.trigger('keydown', { key: 'ArrowDown' });
expect(document.activeElement).toBe(buttons[0].element);
wrapper.unmount();
});
it('ArrowUp moves focus to the previous item', async () => {
const wrapper = mountAttached();
const buttons = wrapper.findAll('button[role="menuitem"]');
await buttons[1].element.focus();
await wrapper
.find('ul[role="menu"]')
.trigger('keydown', { key: 'ArrowUp' });
expect(document.activeElement).toBe(buttons[0].element);
wrapper.unmount();
});
it('ArrowUp wraps from first to last item', async () => {
const wrapper = mountAttached();
const buttons = wrapper.findAll('button[role="menuitem"]');
await buttons[0].element.focus();
await wrapper
.find('ul[role="menu"]')
.trigger('keydown', { key: 'ArrowUp' });
expect(document.activeElement).toBe(buttons[buttons.length - 1].element);
wrapper.unmount();
});
it('Home moves focus to the first item', async () => {
const wrapper = mountAttached();
const buttons = wrapper.findAll('button[role="menuitem"]');
await buttons[2].element.focus();
await wrapper.find('ul[role="menu"]').trigger('keydown', { key: 'Home' });
expect(document.activeElement).toBe(buttons[0].element);
wrapper.unmount();
});
it('End moves focus to the last item', async () => {
const wrapper = mountAttached();
const buttons = wrapper.findAll('button[role="menuitem"]');
await buttons[0].element.focus();
await wrapper.find('ul[role="menu"]').trigger('keydown', { key: 'End' });
expect(document.activeElement).toBe(buttons[buttons.length - 1].element);
wrapper.unmount();
});
it('Escape emits "close"', async () => {
const wrapper = mountAttached();
await wrapper
.find('ul[role="menu"]')
.trigger('keydown', { key: 'Escape' });
expect(wrapper.emitted('close')).toBeTruthy();
wrapper.unmount();
});
it('disabled items are skipped during ArrowDown navigation', async () => {
const itemsWithDisabled = [
{ label: 'First' },
{ label: 'Disabled', disabled: true },
{ label: 'Third' },
];
const wrapper = mount(MActionListbox, {
props: { items: itemsWithDisabled },
attachTo: document.body,
global: { components: { MDivider, MIconButton, Cross24 } },
});
const enabledButtons = wrapper.findAll(
'button[role="menuitem"]:not([disabled])',
);
await enabledButtons[0].element.focus();
await wrapper
.find('ul[role="menu"]')
.trigger('keydown', { key: 'ArrowDown' });
expect(document.activeElement).toBe(enabledButtons[1].element);
wrapper.unmount();
});
});
});