// Shared smoke-test helpers: a fetch mock keyed by "METHOD /path" and an app // mount at a route with a signed-in admin. Fixtures are neutral acme.demo.* // data; no application names appear in framework tests. import { vi } from 'vitest' 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 { 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' export const profile = { data: { id: 7, login: 'dev', email: 'dev@example.test', first_name: 'Dana', last_name: 'Dev', is_superuser: false, role: { id: 2, code: 'developer', name: 'Developer' }, }, meta: {}, } export interface Reply { status?: number body: unknown } export type Route = Reply | ((request: Request) => Reply | Promise) export function json(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } }) } export const notFound = { error: { code: 'not_found', message: 'Not found', details: {} } } /** Replaces fetch; routes are keyed by "METHOD pathname" (query ignored). */ export function mockApi(routes: Record): Request[] { const calls: Request[] = [] vi.spyOn(globalThis, 'fetch').mockImplementation(async (input: RequestInfo | URL) => { const request = input as Request calls.push(request) const route = routes[`${request.method} ${new URL(request.url).pathname}`] if (!route) { return json(404, notFound) } const reply = typeof route === 'function' ? await route(request.clone()) : route return json(reply.status ?? 200, reply.body) }) return calls } export function pathOf(request: Request): string { return new URL(request.url).pathname } export function queryOf(request: Request): URLSearchParams { return new URL(request.url).searchParams } /** Requests matching a method and pathname, in call order. */ export function requestsTo(calls: Request[], method: string, pathname: string): Request[] { return calls.filter((request) => request.method === method && pathOf(request) === pathname) } /** Mounts the whole app at a path with the profile and navigation loaded. */ export async function mountApp(path: string, routes: Record, options: { attach?: boolean } = {}) { const calls = mockApi({ [`GET ${API}/auth/me`]: { body: profile }, [`GET ${API}/navigation`]: { body: navigation }, ...routes, }) await me() await loadNavigation() const router = createAdminRouter(createMemoryHistory()) await router.push(path) const wrapper = mount(App, { global: { plugins: [router] }, attachTo: options.attach ? document.body : undefined, }) 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 = {}): Promise { 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) { const state = { ...initial } const listeners = new Map>() const of = (query: string): Set => { 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()() }