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:
Jakub Zych
2026-09-27 17:53:57 +02:00
parent 8259a456bb
commit 1c2a66df45
61 changed files with 4299 additions and 91 deletions

View 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)
})
})