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

@@ -6,9 +6,15 @@ import { flushPromises, mount } from '@vue/test-utils'
import { createMemoryHistory } from 'vue-router'
import App from '../src/App.vue'
import { createAdminRouter } from '../src/app/router'
import { me } from '../src/state/useAuth'
import { loadNavigation } from '../src/state/useNavigation'
import { setBundle } from '../src/app/i18n'
import { clearUser, me } from '../src/state/useAuth'
import { clearRecordCrumb } from '../src/state/useBreadcrumbs'
import { loadNavigation, setNavigation } from '../src/state/useNavigation'
import { setSettings } from '../src/state/useSettings'
import { watchViewport } from '../src/state/useSidebar'
import { clearToasts } from '../src/state/useToasts'
import navigation from './fixtures/navigation.json'
import { langFixture } from './fixtures/typed'
export const API = '/admin-test/api/v1'
@@ -85,3 +91,81 @@ export async function mountApp(path: string, routes: Record<string, Route>, opti
await flushPromises()
return { wrapper, calls, router }
}
/** Signs the profile in over the mocked API; extra routes join the mock. */
export async function signIn(routes: Record<string, Route> = {}): Promise<Request[]> {
const calls = mockApi({ [`GET ${API}/auth/me`]: { body: profile }, ...routes })
await me()
return calls
}
/** The real admin router on memory history, already at path. */
export async function routerAt(path: string) {
const router = createAdminRouter(createMemoryHistory())
await router.push(path)
await router.isReady()
return router
}
/** Dispatches a bubbling, cancelable keydown and returns the event. */
export function keydown(target: Element, key: string): KeyboardEvent {
const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true })
target.dispatchEvent(event)
return event
}
export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
type MediaListener = (event: MediaQueryListEvent) => void
/** A matchMedia whose answers the test controls, with change events. */
export function fakeMedia(initial: Record<string, boolean>) {
const state = { ...initial }
const listeners = new Map<string, Set<MediaListener>>()
const of = (query: string): Set<MediaListener> => {
let set = listeners.get(query)
if (!set) {
set = new Set()
listeners.set(query, set)
}
return set
}
vi.spyOn(window, 'matchMedia').mockImplementation(
(query: string) =>
({
get matches() {
return state[query] ?? false
},
media: query,
onchange: null,
addEventListener: (_type: string, listener: MediaListener) => of(query).add(listener),
removeEventListener: (_type: string, listener: MediaListener) => of(query).delete(listener),
addListener: () => undefined,
removeListener: () => undefined,
dispatchEvent: () => false,
}) as unknown as MediaQueryList,
)
return {
change(query: string, matches: boolean): void {
state[query] = matches
for (const listener of of(query)) {
listener({ matches, media: query } as MediaQueryListEvent)
}
},
listeners: (query: string) => of(query).size,
}
}
/** Module state back to a signed-out, Polish-strings start. */
export function resetState(): void {
clearUser()
setNavigation([])
setSettings([])
clearToasts()
clearRecordCrumb()
setBundle(langFixture.data, 'pl')
window.localStorage.clear()
// Re-reads the (cleared) preference and the desktop viewport of setup.ts,
// so a narrow or collapsed sidebar never leaks into the next test.
watchViewport()()
}