Files
summercms/admin/tests/helpers.ts
Jakub Zych 126ca5b8ed feat(10-03): open, edit and save a record with toast and 422 feedback
- The SPA loads the backend::lang bundle before /auth/me, sets the
  document language from meta.locale and renders plural messages with
  Intl.PluralRules; interpolate mirrors phrasebook for :name/:Name/:NAME
- Create and record routes; mapWinterUrl maps recordUrl and redirects
  onto the controller's list, create and record routes only
- List rows open their record; FormView loads the form schema and the
  record, shows context-allowed fields in the span grid, saves values
  keyed by field name and toasts the resolved saved message
- A 422 puts each message under its field (aria-invalid,
  aria-describedby), shows the plural banner, focuses the first invalid
  field in schema order and clears a field's error on change
- The D-05 registry maps text, textarea, number and dropdown; any other
  type renders the unsupported-field box with the type in DM Mono
- The admin OpenAPI document declares the write request bodies
  (AdminRecord, AdminIDsRequest) and the list filter query as a
  deepObject, so the typed client can send them
- New backend::lang form.load_failed key; boardwalk/dist rebuilt
2026-09-27 16:43:47 +02:00

88 lines
2.9 KiB
TypeScript

// 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 { me } from '../src/state/useAuth'
import { loadNavigation } from '../src/state/useNavigation'
import navigation from './fixtures/navigation.json'
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<Reply>)
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<string, Route>): 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<string, Route>, 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 }
}