UNPKG

@nuxt/test-utils

Version:
269 lines (268 loc) 10.1 kB
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 };