import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import PluginRail from '../../src/components/shell/PluginRail.vue' import { setNavigation } from '../../src/state/useNavigation' import { setSettings } from '../../src/state/useSettings' import { NARROW_QUERY, SIDEBAR_KEY, setCollapsed, watchViewport } from '../../src/state/useSidebar' import { navigationFixture, settingsFixture } from '../fixtures/typed' import { fakeMedia, keydown, resetState, routerAt, signIn, wait } from '../helpers' async function mountRail(path: string) { await signIn() setNavigation(navigationFixture.data) const router = await routerAt(path) const wrapper = mount(PluginRail, { global: { plugins: [router] }, attachTo: document.body }) await flushPromises() return { wrapper, router } } let stopViewport: (() => void) | null = null beforeEach(() => { resetState() setCollapsed(false) stopViewport = null }) afterEach(() => { stopViewport?.() setCollapsed(false) document.body.innerHTML = '' }) enableAutoUnmount(afterEach) describe('plugin rail', () => { it('is a labelled navigation of the permitted plugins in order, the active one aria-current', async () => { const { wrapper } = await mountRail('/acme/demo/gadgets') const nav = wrapper.find('nav') expect(nav.attributes('aria-label')).toBe('Wtyczki') const items = wrapper.findAll('[data-rail-item]') expect(items.map((item) => item.attributes('data-rail-item'))).toEqual(['demo', 'tools']) expect(items.map((item) => item.text())).toEqual(['Demo', 'Tools']) expect(items[0]!.attributes('aria-current')).toBe('page') expect(items[1]!.attributes('aria-current')).toBeUndefined() expect(items[0]!.attributes('href')).toBe('/acme/demo/widgets') expect(items[1]!.attributes('href')).toBe('/acme/tools/hammers') // Expanded: no popup semantics on the items. expect(items[0]!.attributes('aria-haspopup')).toBeUndefined() expect(items[0]!.attributes('aria-expanded')).toBeUndefined() }) it('shows Ustawienia only with a permitted settings page, current on settings routes', async () => { const { wrapper } = await mountRail('/settings/mail') expect(wrapper.find('[data-rail-settings]').exists()).toBe(false) setSettings(settingsFixture.list.data) await flushPromises() const link = wrapper.find('[data-rail-settings]') expect(link.text()).toBe('Ustawienia') expect(link.attributes('aria-current')).toBe('page') }) it('offers menu semantics and the expand button after a manual collapse', async () => { const { wrapper } = await mountRail('/acme/demo/widgets') expect(wrapper.find('[data-sidebar-expand]').exists()).toBe(false) setCollapsed(true) await flushPromises() const item = wrapper.find('[data-rail-item="demo"]') expect(item.attributes('aria-haspopup')).toBe('menu') expect(item.attributes('aria-expanded')).toBe('false') const expand = wrapper.find('[data-sidebar-expand]') expect(expand.attributes('aria-label')).toBe('RozwiƄ menu') await expand.trigger('click') expect(window.localStorage.getItem(SIDEBAR_KEY)).toBe('false') expect(wrapper.find('[data-sidebar-expand]').exists()).toBe(false) }) it('has no expand button when the viewport forces the collapse', async () => { fakeMedia({ [NARROW_QUERY]: true }) stopViewport = watchViewport() const { wrapper } = await mountRail('/acme/demo/widgets') expect(wrapper.find('[data-rail-item="demo"]').attributes('aria-haspopup')).toBe('menu') expect(wrapper.find('[data-sidebar-expand]').exists()).toBe(false) }) it('opens the flyout on hover, links it by aria-controls and closes it on expand', async () => { const { wrapper } = await mountRail('/acme/demo/widgets') setCollapsed(true) await flushPromises() const item = wrapper.find('[data-rail-item="tools"]') await item.trigger('mouseenter') const flyout = document.querySelector('[data-section-flyout]') expect(flyout?.getAttribute('aria-label')).toBe('Tools') expect(item.attributes('aria-expanded')).toBe('true') expect(item.attributes('aria-controls')).toBe(flyout?.id) setCollapsed(false) await flushPromises() expect(document.querySelector('[data-section-flyout]')).toBeNull() }) it('ignores keys while expanded and closes an open flyout on Escape', async () => { const { wrapper } = await mountRail('/acme/demo/widgets') const item = wrapper.find('[data-rail-item="demo"]').element expect(keydown(item, 'Enter').defaultPrevented).toBe(false) setCollapsed(true) await flushPromises() expect(keydown(item, 'ArrowDown').defaultPrevented).toBe(true) await flushPromises() await wait(0) expect(document.querySelector('[data-section-flyout]')).not.toBeNull() expect(keydown(item, 'Escape').defaultPrevented).toBe(true) await flushPromises() expect(document.querySelector('[data-section-flyout]')).toBeNull() expect(keydown(item, 'Escape').defaultPrevented).toBe(false) }) it('keeps the flyout open while focus stays on its rail item', async () => { const { wrapper } = await mountRail('/acme/demo/widgets') setCollapsed(true) await flushPromises() const item = wrapper.find('[data-rail-item="demo"]') ;(item.element as HTMLElement).focus() await item.trigger('focus') expect(document.querySelector('[data-section-flyout]')).not.toBeNull() await item.trigger('mouseleave') await wait(250) expect(document.querySelector('[data-section-flyout]')).not.toBeNull() ;(item.element as HTMLElement).blur() await item.trigger('blur') await wait(250) await flushPromises() expect(document.querySelector('[data-section-flyout]')).toBeNull() }) })