@untestutils/nuxt 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/dist/config/index.d.mts +58 -0
  3. package/dist/config/index.d.ts +59 -0
  4. package/dist/config/index.d.ts.map +1 -0
  5. package/dist/config/index.mjs +323 -0
  6. package/dist/config/utils.d.mts +12 -0
  7. package/dist/config/utils.d.ts +13 -0
  8. package/dist/config/utils.d.ts.map +1 -0
  9. package/dist/config/utils.mjs +79 -0
  10. package/dist/environment/index.d.mts +22 -0
  11. package/dist/environment/index.d.ts +23 -0
  12. package/dist/environment/index.d.ts.map +1 -0
  13. package/dist/environment/index.mjs +129 -0
  14. package/dist/index.d.mts +36 -0
  15. package/dist/index.d.ts +65 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.mjs +355 -0
  18. package/dist/module/index.d.mts +3 -0
  19. package/dist/module/index.d.ts +4 -0
  20. package/dist/module/index.d.ts.map +1 -0
  21. package/dist/module/index.mjs +294 -0
  22. package/dist/runtime/browser-entry.d.mts +5 -0
  23. package/dist/runtime/browser-entry.d.ts +6 -0
  24. package/dist/runtime/browser-entry.d.ts.map +1 -0
  25. package/dist/runtime/browser-entry.mjs +2 -0
  26. package/dist/runtime/entry.d.mts +1 -0
  27. package/dist/runtime/entry.d.ts +2 -0
  28. package/dist/runtime/entry.d.ts.map +1 -0
  29. package/dist/runtime/entry.mjs +9 -0
  30. package/dist/runtime/index.d.mts +81 -0
  31. package/dist/runtime/index.d.ts +82 -0
  32. package/dist/runtime/index.d.ts.map +1 -0
  33. package/dist/runtime/index.mjs +156 -0
  34. package/dist/runtime/mocks/vue-devtools.d.mts +12 -0
  35. package/dist/runtime/mocks/vue-devtools.d.ts +13 -0
  36. package/dist/runtime/mocks/vue-devtools.d.ts.map +1 -0
  37. package/dist/runtime/mocks/vue-devtools.mjs +12 -0
  38. package/dist/runtime/nuxt-root.d.mts +3 -0
  39. package/dist/runtime/nuxt-root.d.ts +4 -0
  40. package/dist/runtime/nuxt-root.d.ts.map +1 -0
  41. package/dist/runtime/nuxt-root.mjs +29 -0
  42. package/dist/runtime/shared/environment.d.mts +52 -0
  43. package/dist/runtime/shared/environment.d.ts +53 -0
  44. package/dist/runtime/shared/environment.d.ts.map +1 -0
  45. package/dist/runtime/shared/environment.mjs +79 -0
  46. package/dist/runtime/shared/h3-v1.d.mts +7 -0
  47. package/dist/runtime/shared/h3-v1.d.ts +8 -0
  48. package/dist/runtime/shared/h3-v1.d.ts.map +1 -0
  49. package/dist/runtime/shared/h3-v1.mjs +50 -0
  50. package/dist/runtime/shared/h3-v2.d.mts +7 -0
  51. package/dist/runtime/shared/h3-v2.d.ts +8 -0
  52. package/dist/runtime/shared/h3-v2.d.ts.map +1 -0
  53. package/dist/runtime/shared/h3-v2.mjs +32 -0
  54. package/dist/runtime/shared/h3.d.mts +4 -0
  55. package/dist/runtime/shared/h3.d.ts +5 -0
  56. package/dist/runtime/shared/h3.d.ts.map +1 -0
  57. package/dist/runtime/shared/h3.mjs +3 -0
  58. package/dist/runtime/shared/nuxt.d.mts +2 -0
  59. package/dist/runtime/shared/nuxt.d.ts +3 -0
  60. package/dist/runtime/shared/nuxt.d.ts.map +1 -0
  61. package/dist/runtime/shared/nuxt.mjs +17 -0
  62. package/dist/runtime/shared/vue-wrapper-plugin.d.mts +12 -0
  63. package/dist/runtime/shared/vue-wrapper-plugin.d.ts +13 -0
  64. package/dist/runtime/shared/vue-wrapper-plugin.d.ts.map +1 -0
  65. package/dist/runtime/shared/vue-wrapper-plugin.mjs +42 -0
  66. package/dist/runtime/suspended.d.mts +45 -0
  67. package/dist/runtime/suspended.d.ts +46 -0
  68. package/dist/runtime/suspended.d.ts.map +1 -0
  69. package/dist/runtime/suspended.mjs +259 -0
  70. package/package.json +150 -0
@@ -0,0 +1,294 @@
1
+
2
+ import { extname, dirname, join, relative } from "pathe";
3
+ import { readFileSync } from "node:fs";
4
+ import { addVitePlugin, defineNuxtModule, resolveIgnorePatterns, resolvePath } from "@nuxt/kit";
5
+ import { walk } from "estree-walker";
6
+ import MagicString from "magic-string";
7
+ import { createUnplugin } from "unplugin";
8
+ import { fileURLToPath } from "node:url";
9
+ //#region mock transform plugin
10
+ const PLUGIN_NAME = "untestutils:mock-transform";
11
+ const HELPER_MOCK_IMPORT = "mockNuxtImport";
12
+ const HELPER_UNMOCK_IMPORT = "unmockNuxtImport";
13
+ const HELPER_MOCK_COMPONENT = "mockComponent";
14
+ const HELPER_MOCK_HOIST = "__NUXT_VITEST_MOCKS";
15
+ const HELPER_MOCK_HOIST_ORIGINAL = "__NUXT_VITEST_MOCKS_ORIGINAL";
16
+ const HELPER_MOCK_HOIST_PREVIOUS = "__NUXT_VITEST_MOCKS_PREVIOUS";
17
+ const HELPERS_NAME = [
18
+ HELPER_MOCK_IMPORT,
19
+ HELPER_UNMOCK_IMPORT,
20
+ HELPER_MOCK_COMPONENT
21
+ ];
22
+ const createMockPlugin = (ctx) => createUnplugin(() => {
23
+ return {
24
+ name: PLUGIN_NAME,
25
+ enforce: "post",
26
+ vite: {
27
+ transform(code, id) {
28
+ if (!HELPERS_NAME.some((n) => code.includes(n))) return;
29
+ if (id.includes("/node_modules/")) return;
30
+ let ast;
31
+ try {
32
+ ast = this.parse(code);
33
+ } catch {
34
+ return;
35
+ }
36
+ let insertionPoint = 0;
37
+ let hasViImport = false;
38
+ const s = new MagicString(code);
39
+ const mocksImport = [];
40
+ const unmocksFrom = new Set();
41
+ const mocksComponent = [];
42
+ const importPathsList = new Set();
43
+ // @ts-expect-error mismatch between acorn/estree types
44
+ walk(ast, { enter: (node, parent) => {
45
+ const removeCallExpression = (start, end = start) => {
46
+ s.overwrite(isExpressionStatement(parent) ? startOf(parent) : startOf(start), isExpressionStatement(parent) ? endOf(parent) : endOf(end), "");
47
+ };
48
+ const parseMockImportTarget = (importTarget, helperName) => {
49
+ const name = isLiteral(importTarget) ? importTarget.value : isIdentifier(importTarget) ? importTarget.name : undefined;
50
+ if (typeof name !== "string") return this.error(new Error(`The first argument of ${helperName}() must be a string literal or mocked target`), startOf(importTarget));
51
+ return {
52
+ name,
53
+ importItem: ctx.imports.find((_) => name === (_.as || _.name))
54
+ };
55
+ };
56
+ if (isImportDeclaration(node)) {
57
+ if (node.source.value === "vitest" && !hasViImport) {
58
+ if (node.specifiers.find((i) => isImportSpecifier(i) && i.imported.type === "Identifier" && i.imported.name === "vi")) {
59
+ insertionPoint = endOf(node);
60
+ hasViImport = true;
61
+ }
62
+ return;
63
+ }
64
+ }
65
+ if (!isCallExpression(node)) return;
66
+ if (isIdentifier(node.callee) && node.callee.name === HELPER_MOCK_IMPORT) {
67
+ if (node.arguments.length !== 2) return this.error(new Error(`${HELPER_MOCK_IMPORT}() should have exactly 2 arguments`), startOf(node));
68
+ const { name, importItem } = parseMockImportTarget(node.arguments[0], HELPER_MOCK_IMPORT);
69
+ if (!importItem) return this.error(`Cannot find import "${name}" to mock`);
70
+ removeCallExpression(node.arguments[0], node.arguments[1]);
71
+ mocksImport.push({
72
+ name,
73
+ import: importItem,
74
+ factory: code.slice(startOf(node.arguments[1]), endOf(node.arguments[1]))
75
+ });
76
+ }
77
+ if (isIdentifier(node.callee) && node.callee.name === HELPER_UNMOCK_IMPORT) {
78
+ if (node.arguments.length !== 1) return this.error(new Error(`${HELPER_UNMOCK_IMPORT}() should have exactly 1 argument`), startOf(node));
79
+ const { name, importItem } = parseMockImportTarget(node.arguments[0], HELPER_UNMOCK_IMPORT);
80
+ if (!importItem) return this.error(`Cannot find import "${name}" to unmock`);
81
+ removeCallExpression(node.arguments[0]);
82
+ unmocksFrom.add(importItem.from);
83
+ mocksImport.push({
84
+ name,
85
+ import: importItem,
86
+ factory: undefined
87
+ });
88
+ }
89
+ if (isIdentifier(node.callee) && node.callee.name === HELPER_MOCK_COMPONENT) {
90
+ if (node.arguments.length !== 2) return this.error(new Error(`${HELPER_MOCK_COMPONENT}() should have exactly 2 arguments`), startOf(node));
91
+ const componentName = node.arguments[0];
92
+ if (!isLiteral(componentName) || typeof componentName.value !== "string") return this.error(new Error(`The first argument of ${HELPER_MOCK_COMPONENT}() must be a string literal`), startOf(componentName));
93
+ const pathOrName = componentName.value;
94
+ const path = ctx.components.find((_) => _.pascalName === pathOrName || _.kebabName === pathOrName)?.filePath || pathOrName;
95
+ removeCallExpression(node.arguments[1]);
96
+ mocksComponent.push({
97
+ path,
98
+ factory: code.slice(startOf(node.arguments[1]), endOf(node.arguments[1]))
99
+ });
100
+ }
101
+ } });
102
+ if (mocksImport.length === 0 && mocksComponent.length === 0) return;
103
+ const mockLines = [];
104
+ for (const from of unmocksFrom) mockLines.push(`vi.unmock(${JSON.stringify(from)});`);
105
+ for (const [from, mocks] of mapGroupBy(mocksImport, (mock) => mock.import.from)) {
106
+ importPathsList.add(from);
107
+ const quotedFrom = JSON.stringify(from);
108
+ const mockModuleEntry = `globalThis.${HELPER_MOCK_HOIST}[${quotedFrom}]`;
109
+ mockLines.push(`vi.mock(${quotedFrom}, async (importOriginal) => {`, ` if (!${mockModuleEntry} || ${unmocksFrom.has(from)}) {`, ` const original = await importOriginal()`, ` const previous = (${mockModuleEntry} ?? {}).${HELPER_MOCK_HOIST_PREVIOUS} ?? {}`, ` ${mockModuleEntry} = { ...original, ...previous }`, ` ${mockModuleEntry}.${HELPER_MOCK_HOIST_ORIGINAL} = { ...original }`, ` ${mockModuleEntry}.${HELPER_MOCK_HOIST_PREVIOUS} = { ...previous }`, ` }`);
110
+ for (const mock of mocks) {
111
+ const quotedName = JSON.stringify(mock.import.name);
112
+ const original = `${mockModuleEntry}.${HELPER_MOCK_HOIST_ORIGINAL}[${quotedName}]`;
113
+ if (mock.factory === undefined) mockLines.push(` ${mockModuleEntry}[${quotedName}] = ${original}`, ` delete ${mockModuleEntry}.${HELPER_MOCK_HOIST_PREVIOUS}[${quotedName}]`);
114
+ else mockLines.push(` ${mockModuleEntry}[${quotedName}] = await (${mock.factory})(${original})`, ` ${mockModuleEntry}.${HELPER_MOCK_HOIST_PREVIOUS}[${quotedName}] = ${mockModuleEntry}[${quotedName}]`);
115
+ }
116
+ mockLines.push(` return ${mockModuleEntry}`);
117
+ mockLines.push(`});`);
118
+ }
119
+ if (mocksComponent.length) mockLines.push(...mocksComponent.flatMap((mock) => {
120
+ return [
121
+ `vi.mock(${JSON.stringify(mock.path)}, async () => {`,
122
+ ` const factory = (${mock.factory});`,
123
+ ` const result = typeof factory === 'function' ? await factory() : await factory`,
124
+ ` return 'default' in result ? result : { default: result }`,
125
+ "});"
126
+ ];
127
+ }));
128
+ if (!mockLines.length) return;
129
+ s.appendLeft(insertionPoint, [
130
+ ``,
131
+ `vi.hoisted(() => {`,
132
+ ` if(!globalThis.${HELPER_MOCK_HOIST}){`,
133
+ ` vi.stubGlobal(${JSON.stringify(HELPER_MOCK_HOIST)}, {})`,
134
+ ` }`,
135
+ `});`,
136
+ ``
137
+ ].join("\n"));
138
+ if (!hasViImport) s.prepend(`import {vi} from "vitest";\n`);
139
+ s.appendLeft(insertionPoint, "\n" + mockLines.join("\n") + "\n");
140
+ importPathsList.forEach((p) => {
141
+ s.append(`\n import ${JSON.stringify(p)};`);
142
+ });
143
+ return {
144
+ code: s.toString(),
145
+ map: s.generateMap({ hires: true })
146
+ };
147
+ },
148
+ async configResolved(config) {
149
+ const plugins = config.plugins;
150
+ const vitestPlugins = plugins.filter((p) => (p.name === "vite:mocks" || p.name?.startsWith("vitest:")) && (p.enforce || "order" in p && p.order) === "post");
151
+ const lastNuxt = findLastIndex(plugins, (i) => Boolean(i.name?.startsWith("nuxt:")));
152
+ if (lastNuxt === -1) return;
153
+ for (const plugin of vitestPlugins) {
154
+ const index = plugins.indexOf(plugin);
155
+ if (index < lastNuxt) {
156
+ plugins.splice(index, 1);
157
+ plugins.splice(lastNuxt, 0, plugin);
158
+ }
159
+ }
160
+ }
161
+ }
162
+ };
163
+ });
164
+ function findLastIndex(arr, predicate) {
165
+ for (let i = arr.length - 1; i >= 0; i--) if (predicate(arr[i])) return i;
166
+ return -1;
167
+ }
168
+ const isImportDeclaration = (node) => node.type === "ImportDeclaration";
169
+ const isImportSpecifier = (node) => node.type === "ImportSpecifier";
170
+ const isCallExpression = (node) => node.type === "CallExpression";
171
+ const isIdentifier = (node) => node.type === "Identifier";
172
+ const isLiteral = (node) => node.type === "Literal";
173
+ const isExpressionStatement = (node) => node?.type === "ExpressionStatement";
174
+ const startOf = (node) => "range" in node && node.range ? node.range[0] : "start" in node ? node.start : 0;
175
+ const endOf = (node) => "range" in node && node.range ? node.range[1] : "end" in node ? node.end : startOf(node);
176
+ function mapGroupBy(items, keySelector) {
177
+ const map = new Map();
178
+ for (const item of items) {
179
+ const key = keySelector(item);
180
+ if (!map.has(key)) map.set(key, []);
181
+ map.get(key).push(item);
182
+ }
183
+ return map;
184
+ }
185
+ //#endregion
186
+ //#region import mocking setup
187
+ const isTestPluginFile = (src) => src.includes(".spec.") || src.includes(".test.");
188
+ async function setupImportMocking(nuxt) {
189
+ const ctx = {
190
+ components: [],
191
+ imports: []
192
+ };
193
+ let importsCtx;
194
+ nuxt.hook("imports:context", async (ctx) => {
195
+ importsCtx = ctx;
196
+ });
197
+ nuxt.hook("ready", async () => {
198
+ ctx.imports = importsCtx ? await importsCtx.getImports() : [];
199
+ });
200
+ nuxt.hook("components:extend", (_) => {
201
+ ctx.components = _;
202
+ });
203
+ nuxt.hook("imports:sources", (presets) => {
204
+ const idx = presets.findIndex((p) => typeof p === "object" && p !== null && "imports" in p && p.imports?.includes("setInterval"));
205
+ if (idx !== -1) presets.splice(idx, 1);
206
+ });
207
+ nuxt.options.ignore = nuxt.options.ignore.filter((i) => i !== "**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}");
208
+ if (nuxt._ignore) for (const pattern of resolveIgnorePatterns("**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}")) nuxt._ignore.add(`!${pattern}`);
209
+ nuxt.hook("app:resolve", (app) => {
210
+ app.plugins = app.plugins.filter((plugin) => !isTestPluginFile(plugin.src));
211
+ });
212
+ addVitePlugin(createMockPlugin(ctx).vite());
213
+ }
214
+ //#endregion
215
+ //#region nuxt root stub plugin
216
+ const STUB_PLUGIN_NAME = "untestutils:nuxt-root-stub";
217
+ const STUB_ID = "nuxt-vitest-app-entry";
218
+ const NuxtRootStubPlugin = (options) => {
219
+ const extension = extname(options.entry);
220
+ const escapedExt = extension.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
221
+ const entryPath = join(dirname(options.entry), STUB_ID + extension);
222
+ const idFilter = new RegExp(`${STUB_ID}(?:${escapedExt})?$`);
223
+ return {
224
+ name: STUB_PLUGIN_NAME,
225
+ enforce: "pre",
226
+ resolveId: {
227
+ filter: { id: idFilter },
228
+ async handler(id, importer) {
229
+ return importer?.endsWith("index.html") ? id : entryPath;
230
+ }
231
+ },
232
+ load: {
233
+ filter: { id: idFilter },
234
+ async handler() {
235
+ return readFileSync(options.entry, "utf-8").replace("#build/root-component.mjs", options.rootStubPath);
236
+ }
237
+ }
238
+ };
239
+ };
240
+ //#endregion
241
+ function runtimeFile(subpath) {
242
+ const bare = subpath.replace(/^\.\//, "").replace(/\.mjs$/, "");
243
+ return fileURLToPath(new URL(`../runtime/${bare}.mjs`, import.meta.url));
244
+ }
245
+ //#region module
246
+ const untestutilsModule = defineNuxtModule({
247
+ meta: {
248
+ name: "untestutils",
249
+ configKey: "testUtils",
250
+ version: "0.1.8"
251
+ },
252
+ defaults: {},
253
+ async setup(_options, nuxt) {
254
+ if (nuxt.options.test || nuxt.options.dev) await setupImportMocking(nuxt);
255
+
256
+
257
+ if (nuxt.options.test && !nuxt.options.dev) nuxt.hook("app:templates", (app) => {
258
+ const template = app.templates.find((t) => t.filename === "paths.mjs");
259
+ if (!template?.getContents) return;
260
+ const original = template.getContents;
261
+ const inlineAppConfig = JSON.stringify(nuxt.options.app);
262
+ template.getContents = async (data) => {
263
+ return (await original(data)).replace(/^import \{ useRuntimeConfig \} from ['"]nitropack\/runtime['"]\n?/m, "").replace(/const getAppConfig = \(\) => useRuntimeConfig\(\)\.app/, () => `const getAppConfig = () => (${inlineAppConfig})`);
264
+ };
265
+ });
266
+ if (nuxt.options.test || nuxt.options.dev) addVitePlugin(NuxtRootStubPlugin({
267
+ entry: await resolvePath("#app/entry", { alias: nuxt.options.alias }),
268
+ rootStubPath: await resolvePath(runtimeFile("nuxt-root"))
269
+ }));
270
+ if (!nuxt.options.test && !nuxt.options.dev) {
271
+ nuxt.options.vite.define ||= {};
272
+ nuxt.options.vite.define["import.meta.vitest"] = "undefined";
273
+ }
274
+ nuxt.hook("prepare:types", (ctx) => {
275
+ ctx.references.push({ types: "vitest/import-meta" });
276
+ for (const tsConfig of [
277
+ ctx.tsConfig,
278
+ ctx.nodeTsConfig,
279
+ ctx.sharedTsConfig
280
+ ]) {
281
+ if (!tsConfig) continue;
282
+ tsConfig.compilerOptions ||= {};
283
+ tsConfig.compilerOptions.allowImportingTsExtensions = true;
284
+ }
285
+ if (ctx.nodeTsConfig) {
286
+ ctx.nodeTsConfig.include ||= [];
287
+ ctx.nodeTsConfig.include.push(relative(nuxt.options.buildDir, join(nuxt.options.rootDir, "vitest.config.*")));
288
+ if (nuxt.options.workspaceDir !== nuxt.options.rootDir) ctx.nodeTsConfig.include.push(relative(nuxt.options.buildDir, join(nuxt.options.workspaceDir, "vitest.config.*")));
289
+ }
290
+ });
291
+ }
292
+ });
293
+ export default untestutilsModule;
294
+ //#endregion
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Optional browser-mode setup entry (Vitest browser provider).
3
+ * Client DOM environment uses `./entry` instead.
4
+ */
5
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Optional browser-mode setup entry (Vitest browser provider).
3
+ * Client DOM environment uses `./entry` instead.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=browser-entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-entry.d.ts","sourceRoot":"","sources":["../../src/runtime/browser-entry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+
2
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../src/runtime/entry.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ import { setupNuxt } from "./shared/nuxt.mjs";
2
+ import { beforeAll, vi } from "vitest";
3
+ const win = globalThis.window;
4
+ if (typeof globalThis !== "undefined" && win?.__NUXT_VITEST_ENVIRONMENT__) {
5
+ vi.resetModules();
6
+ beforeAll(async () => {
7
+ await setupNuxt();
8
+ });
9
+ }
@@ -0,0 +1,81 @@
1
+ import type { Component, VNode } from "vue";
2
+ import type { MountingOptions, VueWrapper } from "@vue/test-utils";
3
+ import type { render as testingLibraryRender } from "@testing-library/vue";
4
+ type Awaitable<T> = T | Promise<T>;
5
+ type RuntimeH3Event = {
6
+ method?: string;
7
+ path: string;
8
+ url?: URL;
9
+ };
10
+ type RuntimeEndpointHandler<T = unknown> = ((event: RuntimeH3Event) => Awaitable<T>) & {
11
+ __is_handler__?: true;
12
+ };
13
+ type MountSuspendedOptions<T extends Component> = MountingOptions<T> & {
14
+ route?: import("vue-router").RouteLocationRaw | false;
15
+ scoped?: boolean;
16
+ spy?: boolean;
17
+ };
18
+ type TestingLibraryRender = typeof testingLibraryRender;
19
+ type RenderSuspendedOptions<T extends Component> = Parameters<TestingLibraryRender>[1] & MountSuspendedOptions<T>;
20
+ type RenderSuspendedResult<T extends Component> = ReturnType<TestingLibraryRender> & {
21
+ rerender: (props?: Record<string, unknown>) => Promise<void>;
22
+ };
23
+ export type SuspendedHelpers = {
24
+ cleanupAll: () => void;
25
+ patchWrapperSetProps: (wrapper: object, setProps: (props: Record<string, unknown>) => void) => void;
26
+ wrapperSuspended: <TWrapper>(component: Component, options: MountingOptions<Component> & {
27
+ route?: import("vue-router").RouteLocationRaw | false;
28
+ scoped?: boolean;
29
+ spy?: boolean;
30
+ }, config: {
31
+ wrapperFn: (component: Component, options?: unknown) => TWrapper;
32
+ wrappedRender?: (render: () => VNode) => () => VNode;
33
+ suspendedHelperName: string;
34
+ clonedComponentName: string;
35
+ }) => Promise<{
36
+ wrapper: TWrapper & {
37
+ setupState: Record<string, unknown>;
38
+ };
39
+ setProps: (props: Record<string, unknown>) => void;
40
+ }>;
41
+ };
42
+ export interface RegisterEndpointOptions {
43
+ handler: RuntimeEndpointHandler;
44
+ method?: string;
45
+ once?: boolean;
46
+ }
47
+ /**
48
+ * `registerEndpoint` lets you create a Nitro endpoint that returns mocked data.
49
+ * Handy when a component fetches data from an API.
50
+ *
51
+ * @param url endpoint name (e.g. `/test/`)
52
+ * @param options factory that returns mocked data, or an object with
53
+ * `handler`, `method` and `once`.
54
+ */
55
+ export declare function registerEndpoint(url: string, options: RegisterEndpointOptions | RuntimeEndpointHandler): () => void;
56
+ /**
57
+ * `mockNuxtImport` mocks Nuxt's auto-import functionality. This is a macro that
58
+ * is transformed to `vi.mock()` by `untestutils/module`.
59
+ */
60
+ export declare function mockNuxtImport<T = unknown>(_target: string | T, _factory: (original?: T) => T): void;
61
+ /**
62
+ * `unmockNuxtImport` reverts a previous `mockNuxtImport`. This is a macro that
63
+ * is transformed by `untestutils/module`.
64
+ */
65
+ export declare function unmockNuxtImport<T = unknown>(_target: string | T): void;
66
+ /**
67
+ * `mockComponent` replaces a component with a mock. This is a macro that is
68
+ * transformed by `untestutils/module`.
69
+ */
70
+ export declare function mockComponent(_path: string, _component: unknown): void;
71
+ /**
72
+ * `mountSuspended` mounts any Vue component within the Nuxt environment,
73
+ * allowing async setup and access to injections from your Nuxt plugins.
74
+ */
75
+ export declare function mountSuspended<T extends Component>(component: T, options?: MountSuspendedOptions<T>): Promise<VueWrapper>;
76
+ /**
77
+ * `renderSuspended` renders any Vue component within the Nuxt environment using
78
+ * `@testing-library/vue`'s `render`. Requires `@testing-library/vue`.
79
+ */
80
+ export declare function renderSuspended<T extends Component>(component: T, options?: RenderSuspendedOptions<T>): Promise<RenderSuspendedResult<T>>;
81
+ export {};
@@ -0,0 +1,82 @@
1
+ import type { Component, VNode } from 'vue';
2
+ import type { MountingOptions, VueWrapper } from '@vue/test-utils';
3
+ import type { render as testingLibraryRender } from '@testing-library/vue';
4
+ type Awaitable<T> = T | Promise<T>;
5
+ type RuntimeH3Event = {
6
+ method?: string;
7
+ path: string;
8
+ url?: URL;
9
+ };
10
+ type RuntimeEndpointHandler<T = unknown> = ((event: RuntimeH3Event) => Awaitable<T>) & {
11
+ __is_handler__?: true;
12
+ };
13
+ type MountSuspendedOptions<T extends Component> = MountingOptions<T> & {
14
+ route?: import('vue-router').RouteLocationRaw | false;
15
+ scoped?: boolean;
16
+ spy?: boolean;
17
+ };
18
+ type TestingLibraryRender = typeof testingLibraryRender;
19
+ type RenderSuspendedOptions<T extends Component> = Parameters<TestingLibraryRender>[1] & MountSuspendedOptions<T>;
20
+ type RenderSuspendedResult<T extends Component> = ReturnType<TestingLibraryRender> & {
21
+ rerender: (props?: Record<string, unknown>) => Promise<void>;
22
+ };
23
+ export type SuspendedHelpers = {
24
+ cleanupAll: () => void;
25
+ patchWrapperSetProps: (wrapper: object, setProps: (props: Record<string, unknown>) => void) => void;
26
+ wrapperSuspended: <TWrapper>(component: Component, options: MountingOptions<Component> & {
27
+ route?: import('vue-router').RouteLocationRaw | false;
28
+ scoped?: boolean;
29
+ spy?: boolean;
30
+ }, config: {
31
+ wrapperFn: (component: Component, options?: unknown) => TWrapper;
32
+ wrappedRender?: (render: () => VNode) => () => VNode;
33
+ suspendedHelperName: string;
34
+ clonedComponentName: string;
35
+ }) => Promise<{
36
+ wrapper: TWrapper & {
37
+ setupState: Record<string, unknown>;
38
+ };
39
+ setProps: (props: Record<string, unknown>) => void;
40
+ }>;
41
+ };
42
+ export interface RegisterEndpointOptions {
43
+ handler: RuntimeEndpointHandler;
44
+ method?: string;
45
+ once?: boolean;
46
+ }
47
+ /**
48
+ * `registerEndpoint` lets you create a Nitro endpoint that returns mocked data.
49
+ * Handy when a component fetches data from an API.
50
+ *
51
+ * @param url endpoint name (e.g. `/test/`)
52
+ * @param options factory that returns mocked data, or an object with
53
+ * `handler`, `method` and `once`.
54
+ */
55
+ export declare function registerEndpoint(url: string, options: RegisterEndpointOptions | RuntimeEndpointHandler): () => void;
56
+ /**
57
+ * `mockNuxtImport` mocks Nuxt's auto-import functionality. This is a macro that
58
+ * is transformed to `vi.mock()` by `untestutils/module`.
59
+ */
60
+ export declare function mockNuxtImport<T = unknown>(_target: string | T, _factory: (original?: T) => T): void;
61
+ /**
62
+ * `unmockNuxtImport` reverts a previous `mockNuxtImport`. This is a macro that
63
+ * is transformed by `untestutils/module`.
64
+ */
65
+ export declare function unmockNuxtImport<T = unknown>(_target: string | T): void;
66
+ /**
67
+ * `mockComponent` replaces a component with a mock. This is a macro that is
68
+ * transformed by `untestutils/module`.
69
+ */
70
+ export declare function mockComponent(_path: string, _component: unknown): void;
71
+ /**
72
+ * `mountSuspended` mounts any Vue component within the Nuxt environment,
73
+ * allowing async setup and access to injections from your Nuxt plugins.
74
+ */
75
+ export declare function mountSuspended<T extends Component>(component: T, options?: MountSuspendedOptions<T>): Promise<VueWrapper>;
76
+ /**
77
+ * `renderSuspended` renders any Vue component within the Nuxt environment using
78
+ * `@testing-library/vue`'s `render`. Requires `@testing-library/vue`.
79
+ */
80
+ export declare function renderSuspended<T extends Component>(component: T, options?: RenderSuspendedOptions<T>): Promise<RenderSuspendedResult<T>>;
81
+ export {};
82
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,IAAI,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAS3E,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AACnC,KAAK,cAAc,GAAG;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,GAAG,CAAC;CACX,CAAC;AACF,KAAK,sBAAsB,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG;IACrF,cAAc,CAAC,EAAE,IAAI,CAAC;CACvB,CAAC;AAgBF,KAAK,qBAAqB,CAAC,CAAC,SAAS,SAAS,IAAI,eAAe,CAAC,CAAC,CAAC,GAAG;IACrE,KAAK,CAAC,EAAE,OAAO,YAAY,EAAE,gBAAgB,GAAG,KAAK,CAAC;IACtD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AACF,KAAK,oBAAoB,GAAG,OAAO,oBAAoB,CAAC;AACxD,KAAK,sBAAsB,CAAC,CAAC,SAAS,SAAS,IAAI,UAAU,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,GACpF,qBAAqB,CAAC,CAAC,CAAC,CAAC;AAC3B,KAAK,qBAAqB,CAAC,CAAC,SAAS,SAAS,IAAI,UAAU,CAAC,oBAAoB,CAAC,GAAG;IACnF,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D,CAAC;AAGF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,oBAAoB,EAAE,CACpB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,KAC/C,IAAI,CAAC;IACV,gBAAgB,EAAE,CAAC,QAAQ,EACzB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,GAAG;QACpC,KAAK,CAAC,EAAE,OAAO,YAAY,EAAE,gBAAgB,GAAG,KAAK,CAAC;QACtD,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,GAAG,CAAC,EAAE,OAAO,CAAC;KACf,EACD,MAAM,EAAE;QACN,SAAS,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,QAAQ,CAAC;QACjE,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,KAAK,MAAM,KAAK,CAAC;QACrD,mBAAmB,EAAE,MAAM,CAAC;QAC5B,mBAAmB,EAAE,MAAM,CAAC;KAC7B,KACE,OAAO,CAAC;QACX,OAAO,EAAE,QAAQ,GAAG;YAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAC;QAC5D,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KACpD,CAAC,CAAC;CACJ,CAAC;AAuBF,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,sBAAsB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,uBAAuB,GAAG,sBAAsB,GACxD,MAAM,IAAI,CAsBZ;AA8CD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,OAAO,EACxC,OAAO,EAAE,MAAM,GAAG,CAAC,EACnB,QAAQ,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,GAC5B,IAAI,CAIN;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAIvE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAG,IAAI,CAItE;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAAC,CAAC,SAAS,SAAS,EACtD,SAAS,EAAE,CAAC,EACZ,OAAO,GAAE,qBAAqB,CAAC,CAAC,CAAM,GACrC,OAAO,CAAC,UAAU,CAAC,CAiBrB;AAyCD;;;GAGG;AACH,wBAAsB,eAAe,CAAC,CAAC,SAAS,SAAS,EACvD,SAAS,EAAE,CAAC,EACZ,OAAO,GAAE,sBAAsB,CAAC,CAAC,CAAM,GACtC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,CA8BnC"}
@@ -0,0 +1,156 @@
1
+
2
+ import { h, nextTick } from "vue";
3
+ import { mount } from "@vue/test-utils";
4
+
5
+ function importSuspended() {
6
+ return import("./suspended");
7
+ }
8
+ //#region registerEndpoint
9
+ function getEndpointRegistry() {
10
+ const app = window.__app ?? (window.__app = {});
11
+ return app._registeredEndpointRegistry ||= {};
12
+ }
13
+ function findEndpointRegistryHandlers(url) {
14
+ const endpointRegistry = getEndpointRegistry();
15
+ const pathname = url.replace(/[?#].*$/, "");
16
+ for (const [key, handlers] of Object.entries(endpointRegistry)) {
17
+ if (key === url || key === pathname) {
18
+ if (handlers?.length) return handlers;
19
+ }
20
+ }
21
+ }
22
+
23
+ export function registerEndpoint(url, options) {
24
+ const app = typeof globalThis !== "undefined" && "window" in globalThis ? globalThis.window?.__app : undefined;
25
+ if (!app) {
26
+ throw new Error("registerEndpoint() can only be used in an `untestutils` runtime environment");
27
+ }
28
+ const config = typeof options === "function" ? {
29
+ url,
30
+ handler: options,
31
+ method: undefined,
32
+ once: false
33
+ } : {
34
+ ...options,
35
+ url
36
+ };
37
+ config.handler = Object.assign(config.handler, { __is_handler__: true });
38
+ const endpointRegistry = getEndpointRegistry();
39
+ endpointRegistry[url] ||= [];
40
+ endpointRegistry[url].push(config);
41
+ window.__registry.add(url);
42
+ app._registered ||= registerGlobalHandler(app);
43
+ return () => {
44
+ endpointRegistry[url]?.splice(endpointRegistry[url].indexOf(config), 1);
45
+ if (endpointRegistry[url]?.length === 0) window.__registry.delete(url);
46
+ };
47
+ }
48
+ const handler = Object.assign(async (event) => {
49
+ const registeredHandlers = findEndpointRegistryHandlers("url" in event && event.url ? (event.url.pathname + event.url.search).replace(/^\/_/, "") : event.path.replace(/^\/_/, ""));
50
+ const latestHandler = [...registeredHandlers || []].reverse().find((config) => config.method ? event.method === config.method : true);
51
+ if (!latestHandler) return;
52
+ const result = await latestHandler.handler(event);
53
+ if (!latestHandler.once) return result;
54
+ const index = registeredHandlers?.indexOf(latestHandler);
55
+ if (index === undefined || index === -1) return result;
56
+ registeredHandlers?.splice(index, 1);
57
+ if (registeredHandlers?.length === 0) window.__registry.delete(latestHandler.url);
58
+ return result;
59
+ }, { __is_handler__: true });
60
+ function registerGlobalHandler(app) {
61
+ app.use(handler, { match: (eventOrPath, _event) => {
62
+ const url = typeof eventOrPath === "string" ? eventOrPath.replace(/^\/_/, "") : eventOrPath.url ? (eventOrPath.url.pathname + eventOrPath.url.search).replace(/^\/_/, "") : eventOrPath.path.replace(/^\/_/, "");
63
+ const event = _event ?? (typeof eventOrPath === "string" ? undefined : eventOrPath);
64
+ return findEndpointRegistryHandlers(url)?.some((config) => config.method ? event?.method === config.method : true) ?? false;
65
+ } });
66
+ return true;
67
+ }
68
+ //#endregion
69
+ //#region mock macros
70
+
71
+ export function mockNuxtImport(_target, _factory) {
72
+ throw new Error("mockNuxtImport() is a macro and it did not get transpiled. Ensure `untestutils/module` is enabled in your Nuxt test config.");
73
+ }
74
+
75
+ export function unmockNuxtImport(_target) {
76
+ throw new Error("unmockNuxtImport() is a macro and it did not get transpiled. Ensure `untestutils/module` is enabled in your Nuxt test config.");
77
+ }
78
+
79
+ export function mockComponent(_path, _component) {
80
+ throw new Error("mockComponent() is a macro and it did not get transpiled. Ensure `untestutils/module` is enabled in your Nuxt test config.");
81
+ }
82
+ //#endregion
83
+ //#region mountSuspended
84
+
85
+ export async function mountSuspended(component, options = {}) {
86
+ const { cleanupAll, patchWrapperSetProps, wrapperSuspended } = await importSuspended();
87
+ const suspendedHelperName = "MountSuspendedHelper";
88
+ const clonedComponentName = "MountSuspendedComponent";
89
+ cleanupAll();
90
+ const { wrapper, setProps } = await wrapperSuspended(component, options, {
91
+ wrapperFn: (component, options) => mount(component, options),
92
+ suspendedHelperName,
93
+ clonedComponentName
94
+ });
95
+ patchWrapperSetProps(wrapper, setProps);
96
+ return wrappedMountedWrapper(wrapper, wrapper.findComponent({ name: clonedComponentName }));
97
+ }
98
+ function wrappedMountedWrapper(wrapper, component) {
99
+ const wrapperProps = [
100
+ "setProps",
101
+ "emitted",
102
+ "setupState",
103
+ "unmount"
104
+ ];
105
+ return new Proxy(wrapper, { get: (_, prop, receiver) => {
106
+ if (prop === "getCurrentComponent") return getCurrentComponentPatchedProxy;
107
+ const target = wrapperProps.includes(prop) ? wrapper : Reflect.has(component, prop) ? component : wrapper;
108
+ const value = Reflect.get(target, prop, receiver);
109
+ return typeof value === "function" ? value.bind(target) : value;
110
+ } });
111
+ function getCurrentComponentPatchedProxy() {
112
+ const currentComponent = component.getCurrentComponent();
113
+ return new Proxy(currentComponent, { get: (target, prop, receiver) => {
114
+ const value = Reflect.get(target, prop, receiver);
115
+ if (prop === "proxy" && value) return new Proxy(value, { get(o, p, r) {
116
+ if (!Reflect.has(currentComponent.props, p)) {
117
+ const setupState = wrapper.setupState;
118
+ if (setupState && typeof setupState === "object") {
119
+ if (Reflect.has(setupState, p)) return Reflect.get(setupState, p, r);
120
+ }
121
+ }
122
+ return Reflect.get(o, p, r);
123
+ } });
124
+ return value;
125
+ } });
126
+ }
127
+ }
128
+ //#endregion
129
+ //#region renderSuspended
130
+
131
+ export async function renderSuspended(component, options = {}) {
132
+ const { cleanupAll, wrapperSuspended } = await importSuspended();
133
+ const wrapperId = "test-wrapper";
134
+ const suspendedHelperName = "RenderHelper";
135
+ const clonedComponentName = "RenderSuspendedComponent";
136
+
137
+ const { render: wrapperFn } = await import("@testing-library/vue");
138
+ cleanupAll();
139
+ document.getElementById(wrapperId)?.remove();
140
+ const { wrapper, setProps } = await wrapperSuspended(component, options, {
141
+ wrapperFn: (component, options) => wrapperFn(component, options),
142
+ wrappedRender: (render) => () => h({
143
+ inheritAttrs: false,
144
+ render: () => h("div", { id: wrapperId }, render())
145
+ }),
146
+ suspendedHelperName,
147
+ clonedComponentName
148
+ });
149
+ const renderResult = wrapper;
150
+ renderResult.rerender = async (props = {}) => {
151
+ setProps(props);
152
+ await nextTick();
153
+ };
154
+ return renderResult;
155
+ }
156
+ //#endregion