@nuxt/test-utils
Version:
Test utilities for Nuxt
228 lines (227 loc) • 6.54 kB
JavaScript
import { Suspense, effectScope, getCurrentInstance, h, nextTick, onErrorCaptured, reactive } from "vue";
import { defineComponent, h as h$1, tryUseNuxtApp, useNuxtApp, useRouter } from "#imports";
import NuxtRoot from "#build/root-component.mjs";
//#region src/runtime-utils/components/RouterLink.ts
function getUseLink(nuxtApp) {
const linkComponent = nuxtApp.vueApp._context.components.RouterLink;
if (!linkComponent || typeof linkComponent !== "object") return void 0;
if (typeof linkComponent.useLink !== "function") return void 0;
return linkComponent.useLink.bind(linkComponent);
}
const RouterLink = defineComponent({
functional: true,
props: {
to: {
type: [String, Object],
required: true
},
custom: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
ariaCurrentValue: String
},
setup: (props, { slots }) => {
const useLink = getUseLink(useNuxtApp());
if (!useLink) {
const navigate = () => {};
const router = useRouter();
return () => {
const route = router.resolve(props.to);
return props.custom ? slots.default?.({
href: route.href,
navigate,
route
}) : h$1("a", {
href: route.href,
onClick: (e) => {
e.preventDefault();
}
}, slots);
};
}
const link = useLink(props);
return () => {
const route = link.route.value;
const href = link.href.value;
const isActive = link.isActive.value;
const isExactActive = link.isExactActive.value;
return props.custom ? slots.default?.({
href,
navigate: link.navigate,
route,
isActive,
isExactActive
}) : h$1("a", {
href,
onClick: (e) => {
e.preventDefault();
return link.navigate(e);
}
}, slots);
};
}
});
//#endregion
//#region src/runtime-utils/utils/suspended.ts
function cleanupAll() {
for (const fn of (window.__cleanup || []).splice(0)) fn();
}
function addCleanup(fn) {
window.__cleanup ||= [];
window.__cleanup.push(fn);
}
function runEffectScope(fn) {
const scope = effectScope();
addCleanup(() => scope.stop());
return scope.run(fn);
}
function wrapperSuspended(component, options, { wrapperFn, wrappedRender = (fn) => fn, suspendedHelperName, clonedComponentName }) {
const { props = {}, attrs = {} } = options;
const { route = "/", scoped = false, ...wrapperFnOptions } = options;
const vueApp = tryUseNuxtApp()?.vueApp || globalThis.__unctx__.get("nuxt-app").tryUse().vueApp;
const { render: componentRender, setup: componentSetup, ...componentRest } = component;
let wrappedInstance = null;
let setupContext;
let setupState;
const setProps = reactive({});
function patchInstanceAppContext() {
const app = getCurrentInstance()?.appContext.app;
if (!app) return;
for (const [key, value] of Object.entries(vueApp)) {
if (key in app) continue;
app[key] = value;
}
}
const ClonedComponent = {
components: {},
...component,
name: clonedComponentName,
async setup(props, instanceContext) {
const currentInstance = getCurrentInstance();
if (currentInstance) currentInstance.emit = (event, ...args) => {
setupContext.emit(event, ...args);
};
if (!componentSetup) return;
const result = scoped ? await runEffectScope(() => componentSetup(props, setupContext)) : await componentSetup(props, setupContext);
if (wrappedInstance?.exposed) instanceContext.expose(wrappedInstance.exposed);
setupState = result && typeof result === "object" ? result : {};
return result;
}
};
const SuspendedHelper = {
name: suspendedHelperName,
render: () => "",
async setup() {
if (route) await useRouter().replace(route);
return () => h(ClonedComponent, {
...props,
...setProps,
...attrs
}, setupContext.slots);
}
};
return new Promise((resolve, reject) => {
let isMountSettled = false;
const wrapper = wrapperFn({
inheritAttrs: false,
__cssModules: componentRest.__cssModules,
setup: (props, ctx) => {
patchInstanceAppContext();
wrappedInstance = getCurrentInstance();
setupContext = ctx;
const nuxtRootSetupResult = runEffectScope(() => NuxtRoot.setup(props, {
...ctx,
expose: () => {}
}));
onErrorCaptured((error, ...args) => {
if (isMountSettled) return;
isMountSettled = true;
try {
wrappedInstance?.appContext.config.errorHandler?.(error, ...args);
reject(error);
} catch (error) {
reject(error);
}
return false;
});
return nuxtRootSetupResult;
},
render: wrappedRender(() => h(Suspense, { onResolve: () => nextTick().then(() => {
if (isMountSettled) return;
isMountSettled = true;
wrapper.setupState = setupState;
resolve({
wrapper,
setProps: (props) => {
Object.assign(setProps, props);
}
});
}) }, { default: () => h(SuspendedHelper) }))
}, {
...wrapperFnOptions,
global: mergeComponentMountingGlobalOptions(wrapperFnOptions.global, {
config: { globalProperties: makeAllPropertiesEnumerable(vueApp.config.globalProperties) },
directives: vueApp._context.directives,
provide: vueApp._context.provides,
stubs: {
Suspense: false,
[SuspendedHelper.name]: false,
[ClonedComponent.name]: false
},
components: {
...vueApp._context.components,
RouterLink
}
})
});
});
}
function mergeComponentMountingGlobalOptions(options = {}, defaults = {}) {
const compilerOptions = {
...defaults.config?.compilerOptions,
...options.config?.compilerOptions
};
return {
...options,
mixins: [...defaults.mixins || [], ...options.mixins || []],
stubs: {
...defaults.stubs,
...Array.isArray(options.stubs) ? Object.fromEntries(options.stubs.map((n) => [n, true])) : options.stubs
},
plugins: [...defaults.plugins || [], ...options.plugins || []],
components: {
...defaults.components,
...options.components
},
provide: {
...defaults.provide,
...options.provide
},
mocks: {
...defaults.mocks,
...options.mocks
},
config: {
...defaults.config,
...options.config,
...Object.keys(compilerOptions).length ? { compilerOptions } : void 0,
globalProperties: {
...defaults.config?.globalProperties,
...options.config?.globalProperties
}
},
directives: {
...defaults.directives,
...options.directives
}
};
}
function makeAllPropertiesEnumerable(target) {
return {
...target,
...Object.fromEntries(Object.getOwnPropertyNames(target).map((key) => [key, target[key]]))
};
}
//#endregion
export { cleanupAll, wrapperSuspended };