import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import SectionFlyout from '../../src/components/shell/SectionFlyout.vue' import type { NavigationEntry } from '../../src/api/types' import { clone, navigationFixture } from '../fixtures/typed' import { keydown, resetState, routerAt, signIn } from '../helpers' function entryWithThree(): NavigationEntry { const entry = clone(navigationFixture.data[0]!) entry.sideMenu.push({ code: 'gizmos', label: 'Gizmos', icon: 'list', order: 0, controller: 'acme.demo.gizmos', sideMenu: [] }) entry.sideMenu.push({ code: 'broken', label: 'Broken', icon: 'list', order: 0, controller: 'bad', sideMenu: [] }) return entry } async function mountFlyout(path = '/acme/demo/gadgets') { await signIn() const router = await routerAt(path) const wrapper = mount(SectionFlyout, { props: { entry: entryWithThree(), id: 'section-flyout-demo' }, global: { plugins: [router] }, attachTo: document.body, }) await flushPromises() return wrapper } const active = () => document.activeElement?.textContent?.trim() beforeEach(() => { resetState() }) afterEach(() => { document.body.innerHTML = '' }) enableAutoUnmount(afterEach) describe('section flyout', () => { it('is a labelled menu of menuitems outside the tab order, the current one aria-current', async () => { const wrapper = await mountFlyout() const menu = wrapper.find('[role="menu"]') expect(menu.attributes('id')).toBe('section-flyout-demo') expect(menu.attributes('aria-label')).toBe('Demo') const items = wrapper.findAll('[role="menuitem"]') expect(items.map((item) => item.text())).toEqual(['Widgets', 'Gadgets', 'Gizmos']) expect(items.every((item) => item.attributes('tabindex') === '-1')).toBe(true) expect(items[1]!.attributes('aria-current')).toBe('page') expect(items[0]!.attributes('aria-current')).toBeUndefined() }) it('focuses the first item and moves with arrows, Home and End, wrapping around', async () => { const wrapper = await mountFlyout() const menu = wrapper.find('[role="menu"]').element await (wrapper.vm as unknown as { focusFirst: () => Promise }).focusFirst() expect(active()).toBe('Widgets') keydown(document.activeElement!, 'ArrowDown') expect(active()).toBe('Gadgets') keydown(document.activeElement!, 'End') expect(active()).toBe('Gizmos') keydown(document.activeElement!, 'ArrowDown') expect(active()).toBe('Widgets') keydown(document.activeElement!, 'ArrowUp') expect(active()).toBe('Gizmos') keydown(document.activeElement!, 'Home') expect(active()).toBe('Widgets') expect(keydown(menu, 'x').defaultPrevented).toBe(false) }) it('asks the rail to close on Escape and reports Tab and navigation', async () => { const wrapper = await mountFlyout() const menu = wrapper.find('[role="menu"]').element expect(keydown(menu, 'Escape').defaultPrevented).toBe(true) keydown(menu, 'Tab') await wrapper.findAll('[role="menuitem"]')[2]!.trigger('click') expect(wrapper.emitted('escape')).toHaveLength(1) expect(wrapper.emitted('leave')).toHaveLength(1) expect(wrapper.emitted('navigate')).toHaveLength(1) }) it('tells whether an element sits inside it', async () => { const wrapper = await mountFlyout() const vm = wrapper.vm as unknown as { contains: (element: Element | null) => boolean } expect(vm.contains(wrapper.find('[role="menuitem"]').element)).toBe(true) expect(vm.contains(document.body)).toBe(false) expect(vm.contains(null)).toBe(false) }) })