// Bulk actions menu (UI-SPEC S1, D-09), the component on its own. The whole // flow with its confirmation is in tests/list/ListView.test.ts. Fixtures are // neutral acme.* data; no application names appear in framework tests. import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { setBundle } from '../../src/app/i18n' import BulkActionsMenu from '../../src/components/list/BulkActionsMenu.vue' import { langFixture } from '../fixtures/typed' import { resetState } from '../helpers' const LONG = 'Move the selected people to the archive of the previous season and notify their team leads' const actions = [ { name: 'activate', label: 'Activate', confirm: 'Activate the selected people?' }, { name: 'archive', label: LONG }, { name: 'markup', label: '' }, ] function mountMenu(props: { actions?: typeof actions; disabled?: boolean } = {}) { return mount(BulkActionsMenu, { props: { actions: props.actions ?? actions, disabled: props.disabled ?? false }, attachTo: document.body, }) } const trigger = (wrapper: VueWrapper) => wrapper.find('[data-action="bulk-actions"]') const items = () => Array.from(document.body.querySelectorAll('[data-bulk-menu] [data-bulk-action]')) async function open(wrapper: VueWrapper): Promise { await trigger(wrapper).trigger('keydown', { key: 'Enter' }) await flushPromises() } beforeEach(() => { resetState() setBundle({ ...langFixture.data, 'backend::lang.list.bulk_actions': { other: 'Bulk actions' } }, 'en') }) afterEach(() => { document.body.innerHTML = '' }) enableAutoUnmount(afterEach) describe('bulk actions menu component (UI-SPEC S1, D-09)', () => { it('is one outline trigger with its label and a decorative chevron', () => { const wrapper = mountMenu() expect(trigger(wrapper).text()).toBe('Bulk actions') expect(trigger(wrapper).classes()).toContain('border-border-strong') expect(trigger(wrapper).attributes('aria-haspopup')).toBe('menu') expect(trigger(wrapper).find('svg').attributes('aria-hidden')).toBe('true') expect(document.body.querySelector('[data-bulk-menu]')).toBeNull() }) it('keeps its label while disabled and does not open', async () => { const wrapper = mountMenu({ disabled: true }) expect(trigger(wrapper).text()).toBe('Bulk actions') expect(trigger(wrapper).attributes('disabled')).toBeDefined() await open(wrapper) expect(document.body.querySelector('[data-bulk-menu]')).toBeNull() await wrapper.setProps({ disabled: false }) expect(trigger(wrapper).attributes('disabled')).toBeUndefined() }) it('lists the actions in the given order as menu items', async () => { const wrapper = mountMenu() await open(wrapper) const menu = document.body.querySelector('[data-bulk-menu]')! expect(menu.getAttribute('role')).toBe('menu') expect(items().map((item) => item.dataset.bulkAction)).toEqual(['activate', 'archive', 'markup']) expect(items().every((item) => item.getAttribute('role') === 'menuitem')).toBe(true) // Declared order, not alphabetical: a reordered list renders reordered. await wrapper.setProps({ actions: [actions[1]!, actions[0]!] }) await flushPromises() expect(items().map((item) => item.dataset.bulkAction)).toEqual(['archive', 'activate']) }) it('lets a long label wrap inside the menu width and renders markup as text', async () => { const wrapper = mountMenu() await open(wrapper) const menu = document.body.querySelector('[data-bulk-menu]')! expect(Array.from(menu.classList)).toEqual(expect.arrayContaining(['w-[240px]', 'max-w-[320px]'])) const [, long, markup] = items() expect(long!.textContent?.trim()).toBe(LONG) expect(long!.className).not.toMatch(/truncate|whitespace-nowrap|line-clamp/) expect(long!.classList.contains('min-h-10')).toBe(true) expect(markup!.textContent?.trim()).toBe('') expect(markup!.querySelector('img')).toBeNull() }) it('emits the chosen action name once and closes', async () => { const wrapper = mountMenu() await open(wrapper) items()[1]!.click() await flushPromises() expect(wrapper.emitted('select')).toEqual([['archive']]) expect(document.body.querySelector('[data-bulk-menu]')).toBeNull() }) it('exposes focus(), which moves focus to the trigger', () => { const wrapper = mountMenu() const other = document.createElement('button') document.body.appendChild(other) other.focus() expect(document.activeElement).toBe(other) ;(wrapper.vm as unknown as { focus: () => void }).focus() expect(document.activeElement).toBe(trigger(wrapper).element) }) })