test(10-05): bring every SPA module, composable and component under Vitest
- 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
This commit is contained in:
88
admin/tests/shell/SectionFlyout.test.ts
Normal file
88
admin/tests/shell/SectionFlyout.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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<void> }).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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user