- 41 unit and component suites under admin/tests/{app,state,shell,list,form,relation,views,ui}
covering states and a11y roles; every src module is imported by a test
- typed fixture helper assigns each JSON fixture to its generated OpenAPI type
- fix: iconFor ignores inherited object members such as "constructor"
- fix: field controls import ./control instead of the registry (import cycle
left a renderer unregistered depending on module load order)
- fix: dropdown shows the placeholder for an unknown stored value next to an emptyOption
- fix: list announces a failed schema load even when the rows arrive after it
- tailwind no longer scans admin/tests; boardwalk/dist rebuilt
137 lines
5.7 KiB
TypeScript
137 lines
5.7 KiB
TypeScript
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()
|
|
})
|
|
})
|