159 lines
5.3 KiB
JavaScript
159 lines
5.3 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|