/** * Test double for `@inertiajs/vue3` (see SPECS.md §9). * * Implements just the Inertia surface PromptVault uses — `Link`, `Head`, * `router`, `useForm`, `usePage` — and records every interaction on * `inertiaMock` so tests can assert against it. * * Register in each test file that mounts an Inertia-dependent component * (adjust the relative path to this file): * * vi.mock('@inertiajs/vue3', async () => import('../stubs/inertia.js')); * * Behavior notes: * - `router.*` calls are recorded as `{ method, url, data, options }` in * `inertiaMock.routerCalls`. * - `useForm` returns a reactive form whose `post`/`put` apply `transform()` * (if set) and record into `inertiaMock.formSubmissions`. * - Submissions complete synchronously by default. Set * `inertiaMock.deferSubmissions = true` to inspect the form while * `processing === true`, then call `resolvePendingSubmissions()`. * - Set `inertiaMock.failNext = true` to make the next form submission fail: * the stub populates `form.errors` and `page.props.errors` (mirroring * Inertia's validation-error behavior) and invokes the `onError` callback, * which is where the component's `form.clearErrors()` runs. */ import { defineComponent, h, reactive } from 'vue'; export const inertiaMock = { /** Recorded `router.*` calls: `{ method, url, data, options }`. */ routerCalls: [], /** Recorded form submissions: `{ method, url, data, options }`. */ formSubmissions: [], /** Every form created via `useForm()` (for direct assertions). */ forms: [], /** `Head` props recorded per mount: `{ title }`. */ headCalls: [], /** The shared Inertia page object returned by `usePage()`. */ page: reactive({ props: { errors: {}, visit: { url: '/', fullUrl: '/' } } }), /** When true, the next submission fails (fires the `onError` callback). */ failNext: false, /** When true, submissions stay pending until `resolvePendingSubmissions()`. */ deferSubmissions: false, /** Pending submission completions (internal bookkeeping). */ pending: [], }; export function resetInertiaMock() { inertiaMock.routerCalls.length = 0; inertiaMock.formSubmissions.length = 0; inertiaMock.forms.length = 0; inertiaMock.headCalls.length = 0; inertiaMock.page.props.errors = {}; inertiaMock.failNext = false; inertiaMock.deferSubmissions = false; inertiaMock.pending.length = 0; } /** Complete every deferred form submission (runs `onSuccess`/`onError`). */ export function resolvePendingSubmissions() { const pending = [...inertiaMock.pending]; inertiaMock.pending.length = 0; for (const finish of pending) { finish(); } } const record = (method, url, data, options) => inertiaMock.routerCalls.push({ method, url, data, options }); export const router = { get: (url, data = {}, options = {}) => record('get', url, data, options), post: (url, data = {}, options = {}) => record('post', url, data, options), put: (url, data = {}, options = {}) => record('put', url, data, options), delete: (url, data = {}, options = {}) => record('delete', url, data, options), reload: (options = {}) => record('reload', '', {}, options), }; /** Renders a plain anchor so tests can inspect `href`/`class`. */ export const Link = defineComponent({ name: 'Link', props: { href: { type: String, required: true } }, setup(props, { slots, attrs }) { return () => h('a', { ...attrs, href: props.href, 'data-inertia': 'true' }, slots.default ? slots.default() : []); }, }); /** No-op page-title component; records the title for assertions. */ export const Head = defineComponent({ name: 'Head', props: { title: { type: String, default: null } }, setup(props) { inertiaMock.headCalls.push({ title: props.title ?? null }); return () => null; }, }); export function usePage() { return inertiaMock.page; } export function useForm(initial = {}) { let transform = null; const submit = (method, url, options = {}) => { const payload = transform ? transform() : form; const data = {}; for (const key of Object.keys(initial)) { data[key] = payload[key] !== undefined ? payload[key] : form[key]; } form.processing = true; inertiaMock.formSubmissions.push({ method, url, data, options }); const finish = () => { if (inertiaMock.failNext) { inertiaMock.failNext = false; form.errors = { name: ['The server rejected the submission.'] }; inertiaMock.page.props.errors = form.errors; options.onError?.(); } else { options.onSuccess?.({ data }); } form.processing = false; }; if (inertiaMock.deferSubmissions) { inertiaMock.pending.push(finish); return; } finish(); }; const form = reactive({ ...initial, processing: false, errors: {}, transform(fn) { transform = fn; }, clearErrors() { form.errors = {}; }, put(url, options = {}) { submit('put', url, options); }, post(url, options = {}) { submit('post', url, options); }, }); inertiaMock.forms.push(form); return form; }