@nuxt/test-utils
Version:
Test utilities for Nuxt
269 lines (268 loc) • 10.1 kB
JavaScript
import { h, nextTick } from "vue";
import { mount } from "@vue/test-utils";
//#region src/runtime-utils/mock.ts
function getEndpointRegistry() {
const app = window.__app ?? {};
return app._registeredEndpointRegistry ||= {};
}
function findEndpointRegistryHandlers(url) {
const endpointRegistry = getEndpointRegistry();
const pathname = url.replace(/[?#].*$/, "");
for (const [key, handlers] of Object.entries(endpointRegistry)) if (key === url || key === pathname) {
if (handlers?.length) return handlers;
}
}
/**
* `registerEndpoint` allows you create Nitro endpoint that returns mocked data. It can come in handy if you want to test a component that makes requests to API to display some data.
* @param url - endpoint name (e.g. `/test/`).
* @param options - factory function that returns the mocked data or an object containing the `handler`, `method`, and `once` properties.
* - `handler`: the event handler function
* - `method`: (optional) HTTP method to match (e.g., 'GET', 'POST')
* - `once`: (optional) if true, the handler will only be used for the first matching request and then automatically removed
* @example
* ```ts
* import { registerEndpoint } from '@nuxt/test-utils/runtime'
*
* registerEndpoint("/test/", () => ({
* test: "test-field"
* }))
*
* // With once option
* registerEndpoint("/api/user", {
* handler: () => ({ name: "Alice" }),
* once: true
* })
* ```
* @see https://nuxt.com/docs/getting-started/testing#registerendpoint
*/
function registerEndpoint(url, options) {
const app = window.__app;
if (!app) throw new Error("registerEndpoint() can only be used in a `@nuxt/test-utils` runtime environment");
const config = typeof options === "function" ? {
url,
handler: options,
method: void 0,
once: false
} : {
...options,
url
};
config.handler = Object.assign(config.handler, { __is_handler__: true });
const endpointRegistry = getEndpointRegistry();
endpointRegistry[url] ||= [];
endpointRegistry[url].push(config);
window.__registry.add(url);
app._registered ||= registerGlobalHandler(app);
return () => {
endpointRegistry[url]?.splice(endpointRegistry[url].indexOf(config), 1);
if (endpointRegistry[url]?.length === 0) window.__registry.delete(url);
};
}
/**
* `mockNuxtImport` allows you to mock Nuxt's auto import functionality.
* @param _target - name of an import to mock or mocked target.
* @param _factory - factory function that returns mocked import.
* @example
* ```ts
* import { mockNuxtImport } from '@nuxt/test-utils/runtime'
*
* mockNuxtImport('useStorage', () => {
* return () => {
* return { value: 'mocked storage' }
* }
* })
*
* // With mocked target
* mockNuxtImport(useStorage, () => {
* return () => {
* return { value: 'mocked storage' }
* }
* })
* ```
* @example
* ```ts
* // Making partial mock with original implementation
* mockNuxtImport(useRoute, original => vi.fn(original))
* // or (with name based)
* mockNuxtImport('useRoute', original => vi.fn(original))
* // or (with name based, type parameter)
* mockNuxtImport<typeof useRoute>('useRoute', original => vi.fn(original))
*
* // Override in test
* vi.mocked(useRoute).mockImplementation(
* (...args) => ({ ...vi.mocked(useRoute).getMockImplementation()!(...args), path: '/mocked' }),
* )
* ```
* @see https://nuxt.com/docs/getting-started/testing#mocknuxtimport
*/
function mockNuxtImport(_target, _factory) {
throw new Error("mockNuxtImport() is a macro and it did not get transpiled. This may be an internal bug of @nuxt/test-utils.");
}
/**
* `unmockNuxtImport` allows you to unmock Nuxt's auto import functionality.
* @param _target - name of an import to unmock or unmocked target.
* @example
* ```ts
* import { unmockNuxtImport } from '@nuxt/test-utils/runtime'
*
* unmockNuxtImport('useStorage')
*
* // With mocked target
* unmockNuxtImport(useStorage)
* ```
*/
function unmockNuxtImport(_target) {
throw new Error("unmockNuxtImport() is a macro and it did not get transpiled. This may be an internal bug of @nuxt/test-utils.");
}
function mockComponent(_path, _component) {
throw new Error("mockComponent() is a macro and it did not get transpiled. This may be an internal bug of @nuxt/test-utils.");
}
const handler = Object.assign(async (event) => {
const registeredHandlers = findEndpointRegistryHandlers("url" in event && event.url ? (event.url.pathname + event.url.search).replace(/^\/_/, "") : event.path.replace(/^\/_/, ""));
const latestHandler = [...registeredHandlers || []].reverse().find((config) => config.method ? event.method === config.method : true);
if (!latestHandler) return;
const result = await latestHandler.handler(event);
if (!latestHandler.once) return result;
const index = registeredHandlers?.indexOf(latestHandler);
if (index === void 0 || index === -1) return result;
registeredHandlers?.splice(index, 1);
if (registeredHandlers?.length === 0) window.__registry.delete(latestHandler.url);
return result;
}, { __is_handler__: true });
function registerGlobalHandler(app) {
app.use(handler, { match: (...args) => {
const [eventOrPath, _event = eventOrPath] = args;
const url = typeof eventOrPath === "string" ? eventOrPath.replace(/^\/_/, "") : (eventOrPath.url.pathname + eventOrPath.url.search).replace(/^\/_/, "");
const event = _event;
return findEndpointRegistryHandlers(url)?.some((config) => config.method ? event?.method === config.method : true) ?? false;
} });
return true;
}
//#endregion
//#region src/runtime-utils/mount.ts
/**
* `mountSuspended` allows you to mount any vue component within the Nuxt environment, allowing async setup and access to injections from your Nuxt plugins. For example:
*
* ```ts
* // tests/components/SomeComponents.nuxt.spec.ts
* it('can mount some component', async () => {
* const component = await mountSuspended(SomeComponent)
* expect(component.text()).toMatchInlineSnapshot(
* 'This is an auto-imported component'
* )
* })
*
* // tests/App.nuxt.spec.ts
* it('can also mount an app', async () => {
* const component = await mountSuspended(App, { route: '/test' })
* expect(component.html()).toMatchInlineSnapshot(`
* "<div>This is an auto-imported component</div>
* <div> I am a global component </div>
* <div>/</div>
* <a href=\\"/test\\"> Test link </a>"
* `)
* })
* ```
* @param component the component to be tested
* @param options optional options to set up your component
*/
async function mountSuspended(component, options = {}) {
const { cleanupAll, patchWrapperSetProps, wrapperSuspended } = await import("../suspended-DDMM5DdJ.mjs").then((n) => n.r);
const suspendedHelperName = "MountSuspendedHelper";
const clonedComponentName = "MountSuspendedComponent";
cleanupAll();
const { wrapper, setProps } = await wrapperSuspended(component, options, {
wrapperFn: mount,
suspendedHelperName,
clonedComponentName
});
patchWrapperSetProps(wrapper, setProps);
return wrappedMountedWrapper(wrapper, wrapper.findComponent({ name: clonedComponentName }));
}
function wrappedMountedWrapper(wrapper, component) {
const wrapperProps = [
"setProps",
"emitted",
"setupState",
"unmount"
];
return new Proxy(wrapper, { get: (_, prop, receiver) => {
if (prop === "getCurrentComponent") return getCurrentComponentPatchedProxy;
const target = wrapperProps.includes(prop) ? wrapper : Reflect.has(component, prop) ? component : wrapper;
const value = Reflect.get(target, prop, receiver);
return typeof value === "function" ? value.bind(target) : value;
} });
function getCurrentComponentPatchedProxy() {
const currentComponent = component.getCurrentComponent();
return new Proxy(currentComponent, { get: (target, prop, receiver) => {
const value = Reflect.get(target, prop, receiver);
if (prop === "proxy" && value) return new Proxy(value, { get(o, p, r) {
if (!Reflect.has(currentComponent.props, p)) {
const setupState = wrapper.setupState;
if (setupState && typeof setupState === "object") {
if (Reflect.has(setupState, p)) return Reflect.get(setupState, p, r);
}
}
return Reflect.get(o, p, r);
} });
return value;
} });
}
}
//#endregion
//#region src/runtime-utils/render.ts
/**
* `renderSuspended` allows you to mount any vue component within the Nuxt environment, allowing async setup and access to injections from your Nuxt plugins.
*
* This is a wrapper around the `render` function from @testing-libary/vue, and should be used together with
* utilities from that package.
*
* ```ts
* // tests/components/SomeComponents.nuxt.spec.ts
* import { renderSuspended } from '@nuxt/test-utils/runtime'
*
* it('can render some component', async () => {
* const { html } = await renderSuspended(SomeComponent)
* expect(html()).toMatchInlineSnapshot(
* 'This is an auto-imported component'
* )
*
* })
*
* // tests/App.nuxt.spec.ts
* import { renderSuspended } from '@nuxt/test-utils/runtime'
* import { screen } from '@testing-library/vue'
*
* it('can also mount an app', async () => {
* const { html } = await renderSuspended(App, { route: '/test' })
* expect(screen.getByRole('link', { name: 'Test Link' })).toBeVisible()
* })
* ```
* @param component the component to be tested
* @param options optional options to set up your component
*/
async function renderSuspended(component, options = {}) {
const { cleanupAll, wrapperSuspended } = await import("../suspended-DDMM5DdJ.mjs").then((n) => n.r);
const wrapperId = "test-wrapper";
const suspendedHelperName = "RenderHelper";
const clonedComponentName = "RenderSuspendedComponent";
const { render: wrapperFn } = await import("@testing-library/vue");
cleanupAll();
document.getElementById(wrapperId)?.remove();
const { wrapper, setProps } = await wrapperSuspended(component, options, {
wrapperFn,
wrappedRender: (render) => () => h({
inheritAttrs: false,
render: () => h("div", { id: wrapperId }, render())
}),
suspendedHelperName,
clonedComponentName
});
wrapper.rerender = async (props) => {
setProps(props);
await nextTick();
};
return wrapper;
}
//#endregion
export { mockComponent, mockNuxtImport, mountSuspended, registerEndpoint, renderSuspended, unmockNuxtImport };