@nuxt/test-utils 3.19.1 → 3.20.1

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.
package/dist/config.d.mts CHANGED
@@ -24,7 +24,7 @@ interface NuxtEnvironmentOptions {
24
24
  rootDir?: string;
25
25
  /**
26
26
  * The starting URL for your Nuxt window environment
27
- * @default {http://localhost:3000}
27
+ * @default 'http://localhost:3000'
28
28
  */
29
29
  url?: string;
30
30
  /**
@@ -37,14 +37,14 @@ interface NuxtEnvironmentOptions {
37
37
  overrides?: NuxtConfig;
38
38
  /**
39
39
  * The id of the root div to which the app should be mounted. You should also set `app.rootId` to the same value.
40
- * @default {nuxt-test}
40
+ * @default 'nuxt-test'
41
41
  */
42
42
  rootId?: string;
43
43
  /**
44
44
  * The name of the DOM environment to use.
45
45
  *
46
46
  * It also needs to be installed as a dev dependency in your project.
47
- * @default {happy-dom}
47
+ * @default 'happy-dom'
48
48
  */
49
49
  domEnvironment?: 'happy-dom' | 'jsdom';
50
50
  mock?: {
package/dist/config.mjs CHANGED
@@ -226,15 +226,19 @@ function defineVitestConfig(config = {}) {
226
226
  if (resolvedConfig.test.browser?.enabled) {
227
227
  return resolvedConfig;
228
228
  }
229
- if ("workspace" in resolvedConfig.test) {
229
+ if ("workspace" in resolvedConfig.test || "projects" in resolvedConfig.test) {
230
230
  throw new Error(
231
- "The `workspace` option is not supported with `defineVitestConfig`. Instead, use `defineVitestProject` to define each workspace project that uses the Nuxt environment."
231
+ "The `projects` option is not supported with `defineVitestConfig`. Instead, use `defineVitestProject` to define each workspace project that uses the Nuxt environment."
232
232
  );
233
233
  }
234
234
  const defaultEnvironment = resolvedConfig.test.environment || "node";
235
235
  if (defaultEnvironment !== "nuxt") {
236
- resolvedConfig.test.workspace = [];
237
- resolvedConfig.test.workspace.push({
236
+ const key = "projects" in resolvedConfig.test ? "projects" : "workspace" in resolvedConfig.test ? "workspace" : await import('vitest/package.json', { with: { type: 'json' } }).then((r) => {
237
+ const [major, minor] = (r.default || r).version.split(".");
238
+ return Number.parseInt(major, 10) > 3 || Number.parseInt(major, 10) === 3 && Number.parseInt(minor, 10) >= 2;
239
+ }) ? "projects" : "workspace";
240
+ resolvedConfig.test[key] = [];
241
+ resolvedConfig.test[key].push({
238
242
  extends: true,
239
243
  test: {
240
244
  name: "nuxt",
@@ -245,6 +249,22 @@ function defineVitestConfig(config = {}) {
245
249
  ]
246
250
  }
247
251
  });
252
+ resolvedConfig.test[key].push({
253
+ extends: true,
254
+ test: {
255
+ name: defaultEnvironment,
256
+ environment: defaultEnvironment,
257
+ exclude: [
258
+ "**/node_modules/**",
259
+ "**/dist/**",
260
+ "**/cypress/**",
261
+ "**/.{idea,git,cache,output,temp}/**",
262
+ "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*",
263
+ "./**/*.nuxt.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}",
264
+ "./{test,tests}/nuxt/**.*"
265
+ ]
266
+ }
267
+ });
248
268
  }
249
269
  return resolvedConfig;
250
270
  });
package/dist/e2e.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { T as TestOptions, a as TestContext, b as TestHooks } from './shared/test-utils.DtJCg4f3.mjs';
2
- export { $ as $fetch, G as GotoOptions, N as NuxtPage, S as StartServerOptions, h as TestRunner, c as createBrowser, d as createPage, f as fetch, g as getBrowser, s as startServer, e as stopServer, u as url, w as waitForHydration } from './shared/test-utils.DtJCg4f3.mjs';
1
+ import { T as TestOptions, a as TestContext, b as TestHooks } from './shared/test-utils.20kU0tZa.mjs';
2
+ export { $ as $fetch, G as GotoOptions, N as NuxtPage, S as StartServerOptions, h as TestRunner, c as createBrowser, d as createPage, f as fetch, g as getBrowser, s as startServer, e as stopServer, u as url, w as waitForHydration } from './shared/test-utils.20kU0tZa.mjs';
3
3
  import { LogType } from 'consola';
4
4
  import 'playwright-core';
5
5
  import '@nuxt/schema';
package/dist/e2e.mjs CHANGED
@@ -1,6 +1,6 @@
1
- export { b as buildFixture, c as createBrowser, a as createPage, d as createTest, g as getBrowser, l as loadFixture, e as setup, s as setupMaps, w as waitForHydration } from './shared/test-utils.CT3RJOY3.mjs';
2
- import { u as useTestContext } from './shared/test-utils.B8qEdk9k.mjs';
3
- export { $ as $fetch, c as createTestContext, e as exposeContextToEnv, f as fetch, i as isDev, r as recoverContextFromEnv, s as setTestContext, a as startServer, b as stopServer, d as url } from './shared/test-utils.B8qEdk9k.mjs';
1
+ export { b as buildFixture, c as createBrowser, a as createPage, d as createTest, g as getBrowser, l as loadFixture, e as setup, s as setupMaps, w as waitForHydration } from './shared/test-utils.CtoalVlS.mjs';
2
+ import { u as useTestContext } from './shared/test-utils.CtwoJP76.mjs';
3
+ export { $ as $fetch, c as createTestContext, e as exposeContextToEnv, f as fetch, i as isDev, r as recoverContextFromEnv, s as setTestContext, a as startServer, b as stopServer, d as url } from './shared/test-utils.CtwoJP76.mjs';
4
4
  import { consola } from 'consola';
5
5
  import { resolve } from 'pathe';
6
6
  import { distDir } from '#dirs';
@@ -1,6 +1,6 @@
1
1
  import { resolve } from 'pathe';
2
2
  import { stringifyQuery } from 'ufo';
3
- import { $ as $fetch, u as useTestContext } from './shared/test-utils.B8qEdk9k.mjs';
3
+ import { $ as $fetch, u as useTestContext } from './shared/test-utils.CtwoJP76.mjs';
4
4
  import 'tinyexec';
5
5
  import 'get-port-please';
6
6
  import 'ofetch';
package/dist/module.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import { pathToFileURL } from 'node:url';
2
- import { useNuxt, resolveIgnorePatterns, addVitePlugin, defineNuxtModule, createResolver, resolvePath, logger } from '@nuxt/kit';
3
- import { mergeConfig } from 'vite';
2
+ import { useNuxt, resolveIgnorePatterns, addVitePlugin, defineNuxtModule, createResolver, resolvePath, importModule, logger } from '@nuxt/kit';
4
3
  import { getPort } from 'get-port-please';
5
4
  import { h } from 'vue';
6
5
  import { debounce } from 'perfect-debounce';
@@ -13,6 +12,7 @@ import { createUnplugin } from 'unplugin';
13
12
  import { readFileSync } from 'node:fs';
14
13
  import { extname, join, dirname } from 'pathe';
15
14
  import 'node:process';
15
+ import 'vite';
16
16
  import 'c12';
17
17
  import 'destr';
18
18
  import 'scule';
@@ -217,7 +217,7 @@ vi.hoisted(() => {
217
217
  const vitestPlugins = plugins.filter((p) => (p.name === "vite:mocks" || p.name.startsWith("vitest:")) && (p.enforce || "order" in p && p.order) === "post");
218
218
  const lastNuxt = findLastIndex(
219
219
  plugins,
220
- (i) => i.name?.startsWith("nuxt:")
220
+ (i) => !!i?.name?.startsWith("nuxt:")
221
221
  );
222
222
  if (lastNuxt === -1) return;
223
223
  for (const plugin of vitestPlugins) {
@@ -279,7 +279,7 @@ function setupImportMocking() {
279
279
  ctx.components = _;
280
280
  });
281
281
  nuxt.hook("imports:sources", (presets) => {
282
- const idx = presets.findIndex((p) => p.imports.includes("setInterval"));
282
+ const idx = presets.findIndex((p) => p.imports?.includes("setInterval"));
283
283
  if (idx !== -1) {
284
284
  presets.splice(idx, 1);
285
285
  }
@@ -360,6 +360,7 @@ const module = defineNuxtModule({
360
360
  }, 100);
361
361
  let URL;
362
362
  async function start() {
363
+ const { mergeConfig } = await importModule("vite", { paths: nuxt.options.modulesDir });
363
364
  const rawViteConfig = mergeConfig({}, await rawViteConfigPromise);
364
365
  const viteConfig = await getVitestConfigFromNuxt({ nuxt, viteConfig: defu({ test: options.vitestConfig }, rawViteConfig) });
365
366
  viteConfig.plugins = (viteConfig.plugins || []).filter((p) => {
@@ -1,7 +1,7 @@
1
1
  import * as _playwright_test from '@playwright/test';
2
2
  export { expect } from '@playwright/test';
3
3
  import { Response } from 'playwright-core';
4
- import { T as TestOptions$1, G as GotoOptions, b as TestHooks } from './shared/test-utils.DtJCg4f3.mjs';
4
+ import { T as TestOptions$1, G as GotoOptions, b as TestHooks } from './shared/test-utils.20kU0tZa.mjs';
5
5
  import '@nuxt/schema';
6
6
  import 'tinyexec';
7
7
 
@@ -1,14 +1,14 @@
1
1
  import defu from 'defu';
2
2
  import { test as test$1 } from '@playwright/test';
3
3
  export { expect } from '@playwright/test';
4
- import { w as waitForHydration, d as createTest } from './shared/test-utils.CT3RJOY3.mjs';
4
+ import { w as waitForHydration, d as createTest } from './shared/test-utils.CtoalVlS.mjs';
5
5
  import 'node:path';
6
6
  import 'ufo';
7
7
  import 'std-env';
8
8
  import 'consola';
9
9
  import 'node:fs';
10
10
  import '@nuxt/kit';
11
- import { d as url } from './shared/test-utils.B8qEdk9k.mjs';
11
+ import { d as url } from './shared/test-utils.CtwoJP76.mjs';
12
12
  import 'pathe';
13
13
  import '#dirs';
14
14
  import 'tinyexec';
@@ -3,3 +3,9 @@ export declare function createRpcServer(): void;
3
3
  export declare const devtools: {
4
4
  init(): void;
5
5
  };
6
+ export declare function addCustomCommand(): void;
7
+ export declare function addCustomTab(): void;
8
+ export declare function onDevToolsClientConnected(): void;
9
+ export declare function onDevToolsConnected(): void;
10
+ export declare function removeCustomCommand(): void;
11
+ export declare function setupDevToolsPlugin(): void;
@@ -5,3 +5,15 @@ export const devtools = {
5
5
  init() {
6
6
  }
7
7
  };
8
+ export function addCustomCommand() {
9
+ }
10
+ export function addCustomTab() {
11
+ }
12
+ export function onDevToolsClientConnected() {
13
+ }
14
+ export function onDevToolsConnected() {
15
+ }
16
+ export function removeCustomCommand() {
17
+ }
18
+ export function setupDevToolsPlugin() {
19
+ }
@@ -36,26 +36,51 @@ export async function setupWindow(win, environmentOptions) {
36
36
  app.id = rootId;
37
37
  win.document.body.appendChild(app);
38
38
  const h3App = createApp();
39
- if (!win.fetch) {
39
+ if (!win.fetch || !("Request" in win)) {
40
40
  await import("node-fetch-native/polyfill");
41
41
  win.URLSearchParams = globalThis.URLSearchParams;
42
+ win.Request ??= class Request extends globalThis.Request {
43
+ constructor(input, init) {
44
+ if (typeof input === "string") {
45
+ super(new URL(input, win.location.origin), init);
46
+ } else {
47
+ super(input, init);
48
+ }
49
+ }
50
+ };
42
51
  }
43
52
  const nodeHandler = toNodeListener(h3App);
44
53
  const registry = /* @__PURE__ */ new Set();
45
- win.fetch = async (url, init) => {
46
- if (typeof url === "string") {
47
- const base = url.split("?")[0];
48
- if (registry.has(base) || registry.has(url)) {
49
- url = "/_" + url;
50
- }
51
- if (url.startsWith("/")) {
52
- const response = await fetchNodeRequestHandler(nodeHandler, url, init);
53
- return normalizeFetchResponse(response);
54
- }
54
+ const _fetch = fetch;
55
+ win.fetch = async (input, _init) => {
56
+ let url;
57
+ let init = _init;
58
+ if (typeof input === "string") {
59
+ url = input;
60
+ } else if (input instanceof URL) {
61
+ url = input.toString();
62
+ } else {
63
+ url = input.url;
64
+ init = {
65
+ method: init?.method ?? input.method,
66
+ body: init?.body ?? input.body,
67
+ headers: init?.headers ?? input.headers
68
+ };
69
+ }
70
+ const base = url.split("?")[0];
71
+ if (registry.has(base) || registry.has(url)) {
72
+ url = "/_" + url;
55
73
  }
56
- return fetch(url, init);
74
+ if (url.startsWith("/")) {
75
+ const response = await fetchNodeRequestHandler(nodeHandler, url, init);
76
+ return normalizeFetchResponse(response);
77
+ }
78
+ return _fetch(input, _init);
57
79
  };
58
80
  win.$fetch = createFetch({ fetch: win.fetch, Headers: win.Headers });
81
+ win.$fetch.create = (options = {}) => {
82
+ return createFetch({ fetch: win.fetch, Headers: win.Headers, ...options });
83
+ };
59
84
  win.__registry = registry;
60
85
  win.__app = h3App;
61
86
  const timestamp = Date.now();
@@ -67,6 +67,7 @@ declare function mockComponent<PropsOptions extends Readonly<ComponentPropsOptio
67
67
 
68
68
  type MountSuspendedOptions<T> = ComponentMountingOptions<T> & {
69
69
  route?: RouteLocationRaw;
70
+ scoped?: boolean;
70
71
  };
71
72
  type SetupState$1 = Record<string, any>;
72
73
  /**
@@ -98,6 +99,9 @@ type SetupState$1 = Record<string, any>;
98
99
  declare function mountSuspended<T>(component: T, options?: MountSuspendedOptions<T>): Promise<ReturnType<typeof mount<T>> & {
99
100
  setupState: SetupState$1;
100
101
  }>;
102
+ declare global {
103
+ var __cleanup: Array<() => void> | undefined;
104
+ }
101
105
 
102
106
  type RenderOptions<C = unknown> = RenderOptions$1<C> & {
103
107
  route?: RouteLocationRaw;
@@ -1,6 +1,6 @@
1
1
  import { defineEventHandler } from 'h3';
2
2
  import { mount } from '@vue/test-utils';
3
- import { reactive, h as h$1, Suspense, nextTick, getCurrentInstance, unref, isReadonly, defineComponent as defineComponent$1, effectScope } from 'vue';
3
+ import { reactive, h as h$1, Suspense, nextTick, effectScope, unref, isReadonly, getCurrentInstance, isRef, defineComponent as defineComponent$1 } from 'vue';
4
4
  import { defu } from 'defu';
5
5
  import { defineComponent, useRouter, h, tryUseNuxtApp } from '#imports';
6
6
  import NuxtRoot from '#build/root-component.mjs';
@@ -19,17 +19,17 @@ function registerEndpoint(url, options) {
19
19
  if (!hasBeenRegistered) {
20
20
  window.__registry.add(url);
21
21
  app.use("/_" + url, defineEventHandler((event) => {
22
- const latestHandler = [...endpointRegistry[url]].reverse().find((config2) => config2.method ? event.method === config2.method : true);
22
+ const latestHandler = [...endpointRegistry[url] || []].reverse().find((config2) => config2.method ? event.method === config2.method : true);
23
23
  return latestHandler?.handler(event);
24
24
  }), {
25
25
  match(_, event) {
26
- return endpointRegistry[url]?.some((config2) => config2.method ? event?.method === config2.method : true);
26
+ return endpointRegistry[url]?.some((config2) => config2.method ? event?.method === config2.method : true) ?? false;
27
27
  }
28
28
  });
29
29
  }
30
30
  return () => {
31
- endpointRegistry[url].splice(endpointRegistry[url].indexOf(config), 1);
32
- if (endpointRegistry[url].length === 0) {
31
+ endpointRegistry[url]?.splice(endpointRegistry[url].indexOf(config), 1);
32
+ if (endpointRegistry[url]?.length === 0) {
33
33
  window.__registry.delete(url);
34
34
  }
35
35
  };
@@ -87,16 +87,50 @@ async function mountSuspended(component, options) {
87
87
  route = "/",
88
88
  ..._options
89
89
  } = options || {};
90
+ for (const cleanupFunction of globalThis.__cleanup || []) {
91
+ cleanupFunction();
92
+ }
90
93
  const vueApp = tryUseNuxtApp()?.vueApp || globalThis.__unctx__.get("nuxt-app").tryUse().vueApp;
91
94
  const { render, setup, data, computed, methods } = component;
92
95
  let setupContext;
93
96
  let setupState;
94
97
  const setProps = reactive({});
98
+ let interceptedEmit = null;
99
+ function getInterceptedEmitFunction(emit) {
100
+ if (emit !== interceptedEmit) {
101
+ interceptedEmit = interceptedEmit ?? ((event, ...args) => {
102
+ emit(event, ...args);
103
+ setupContext.emit(event, ...args);
104
+ });
105
+ }
106
+ return interceptedEmit;
107
+ }
108
+ function interceptEmitOnCurrentInstance() {
109
+ const currentInstance = getCurrentInstance();
110
+ if (!currentInstance) {
111
+ return;
112
+ }
113
+ currentInstance.emit = getInterceptedEmitFunction(currentInstance.emit);
114
+ }
95
115
  let passedProps;
116
+ let componentScope = null;
96
117
  const wrappedSetup = async (props2, setupContext2) => {
118
+ interceptEmitOnCurrentInstance();
97
119
  passedProps = props2;
98
120
  if (setup) {
99
- const result = await setup(props2, setupContext2);
121
+ let result;
122
+ if (options?.scoped) {
123
+ componentScope = effectScope();
124
+ globalThis.__cleanup ||= [];
125
+ globalThis.__cleanup.push(() => {
126
+ componentScope?.stop();
127
+ });
128
+ result = await componentScope?.run(async () => {
129
+ return await setup(props2, setupContext2);
130
+ });
131
+ } else {
132
+ result = await setup(props2, setupContext2);
133
+ }
100
134
  setupState = result && typeof result === "object" ? result : {};
101
135
  return result;
102
136
  }
@@ -107,11 +141,24 @@ async function mountSuspended(component, options) {
107
141
  {
108
142
  setup: (props2, ctx) => {
109
143
  setupContext = ctx;
110
- return NuxtRoot.setup(props2, {
111
- ...ctx,
112
- expose: () => {
113
- }
114
- });
144
+ if (options?.scoped) {
145
+ const scope = effectScope();
146
+ globalThis.__cleanup ||= [];
147
+ globalThis.__cleanup.push(() => {
148
+ scope.stop();
149
+ });
150
+ return scope.run(() => NuxtRoot.setup(props2, {
151
+ ...ctx,
152
+ expose: () => {
153
+ }
154
+ }));
155
+ } else {
156
+ return NuxtRoot.setup(props2, {
157
+ ...ctx,
158
+ expose: () => {
159
+ }
160
+ });
161
+ }
115
162
  },
116
163
  render: (renderContext) => h$1(
117
164
  Suspense,
@@ -134,14 +181,7 @@ async function mountSuspended(component, options) {
134
181
  name: "MountSuspendedComponent",
135
182
  ...component,
136
183
  render: render ? function(_ctx, ...args) {
137
- const currentInstance = getCurrentInstance();
138
- if (currentInstance) {
139
- const oldEmit = currentInstance.emit;
140
- currentInstance.emit = (event, ...args2) => {
141
- oldEmit(event, ...args2);
142
- setupContext.emit(event, ...args2);
143
- };
144
- }
184
+ interceptEmitOnCurrentInstance();
145
185
  if (data && typeof data === "function") {
146
186
  const dataObject = data();
147
187
  for (const key in dataObject) {
@@ -170,18 +210,22 @@ async function mountSuspended(component, options) {
170
210
  propsContext[key] = passedProps[key];
171
211
  }
172
212
  if (methods && typeof methods === "object") {
173
- for (const key in methods) {
174
- renderContext[key] = methods[key].bind(renderContext);
213
+ for (const [key, value] of Object.entries(methods)) {
214
+ renderContext[key] = value.bind(renderContext);
175
215
  }
176
216
  }
177
217
  if (computed && typeof computed === "object") {
178
- for (const key in computed) {
179
- renderContext[key] = computed[key].call(renderContext);
218
+ for (const [key, value] of Object.entries(computed)) {
219
+ if ("get" in value) {
220
+ renderContext[key] = value.get.call(renderContext);
221
+ } else {
222
+ renderContext[key] = value.call(renderContext);
223
+ }
180
224
  }
181
225
  }
182
226
  return render.call(this, renderContext, ...args);
183
227
  } : void 0,
184
- setup: setup ? (props2) => wrappedSetup(props2, setupContext) : void 0
228
+ setup: (props2) => wrappedSetup(props2, setupContext)
185
229
  };
186
230
  return () => h$1(clonedComponent, { ...props, ...setProps, ...attrs }, slots);
187
231
  }
@@ -226,6 +270,9 @@ function wrappedMountedWrapper(wrapper) {
226
270
  if (prop === "element") {
227
271
  const component = target.findComponent({ name: "MountSuspendedComponent" });
228
272
  return component[prop];
273
+ } else if (prop === "vm") {
274
+ const vm = Reflect.get(target, prop, receiver);
275
+ return createVMProxy(vm, wrapper.setupState);
229
276
  } else {
230
277
  return Reflect.get(target, prop, receiver);
231
278
  }
@@ -241,6 +288,28 @@ function wrappedMountedWrapper(wrapper) {
241
288
  }
242
289
  return proxy;
243
290
  }
291
+ function createVMProxy(vm, setupState) {
292
+ return new Proxy(vm, {
293
+ get(target, key, receiver) {
294
+ const value = Reflect.get(target, key, receiver);
295
+ if (setupState && typeof setupState === "object" && key in setupState) {
296
+ return unref(setupState[key]);
297
+ }
298
+ return value;
299
+ },
300
+ set(target, key, value, receiver) {
301
+ if (setupState && typeof setupState === "object" && key in setupState) {
302
+ const setupValue = setupState[key];
303
+ if (setupValue && isRef(setupValue)) {
304
+ setupValue.value = value;
305
+ return true;
306
+ }
307
+ return Reflect.set(setupState, key, value, receiver);
308
+ }
309
+ return Reflect.set(target, key, value, receiver);
310
+ }
311
+ });
312
+ }
244
313
 
245
314
  const WRAPPER_EL_ID = "test-wrapper";
246
315
  async function renderSuspended(component, options) {
@@ -256,12 +325,30 @@ async function renderSuspended(component, options) {
256
325
  const { render, setup, data, computed, methods } = component;
257
326
  let setupContext;
258
327
  let setupState;
328
+ let interceptedEmit = null;
329
+ function getInterceptedEmitFunction(emit) {
330
+ if (emit !== interceptedEmit) {
331
+ interceptedEmit = interceptedEmit ?? ((event, ...args) => {
332
+ emit(event, ...args);
333
+ setupContext.emit(event, ...args);
334
+ });
335
+ }
336
+ return interceptedEmit;
337
+ }
338
+ function interceptEmitOnCurrentInstance() {
339
+ const currentInstance = getCurrentInstance();
340
+ if (!currentInstance) {
341
+ return;
342
+ }
343
+ currentInstance.emit = getInterceptedEmitFunction(currentInstance.emit);
344
+ }
259
345
  for (const fn of window.__cleanup || []) {
260
346
  fn();
261
347
  }
262
348
  document.querySelector(`#${WRAPPER_EL_ID}`)?.remove();
263
349
  let passedProps;
264
350
  const wrappedSetup = async (props2, setupContext2) => {
351
+ interceptEmitOnCurrentInstance();
265
352
  passedProps = props2;
266
353
  if (setup) {
267
354
  const result = await setup(props2, setupContext2);
@@ -316,6 +403,7 @@ async function renderSuspended(component, options) {
316
403
  name: "RenderSuspendedComponent",
317
404
  ...component,
318
405
  render: render ? function(_ctx, ...args) {
406
+ interceptEmitOnCurrentInstance();
319
407
  if (data && typeof data === "function") {
320
408
  const dataObject = data();
321
409
  for (const key in dataObject) {
@@ -344,18 +432,22 @@ async function renderSuspended(component, options) {
344
432
  propsContext[key] = passedProps[key];
345
433
  }
346
434
  if (methods && typeof methods === "object") {
347
- for (const key in methods) {
348
- renderContext[key] = methods[key].bind(renderContext);
435
+ for (const [key, value] of Object.entries(methods)) {
436
+ renderContext[key] = value.bind(renderContext);
349
437
  }
350
438
  }
351
439
  if (computed && typeof computed === "object") {
352
- for (const key in computed) {
353
- renderContext[key] = computed[key].call(renderContext);
440
+ for (const [key, value] of Object.entries(computed)) {
441
+ if ("get" in value) {
442
+ renderContext[key] = value.get.call(renderContext);
443
+ } else {
444
+ renderContext[key] = value.call(renderContext);
445
+ }
354
446
  }
355
447
  }
356
448
  return render.call(this, renderContext, ...args);
357
449
  } : void 0,
358
- setup: setup ? (props2) => wrappedSetup(props2, setupContext) : void 0
450
+ setup: (props2) => wrappedSetup(props2, setupContext)
359
451
  };
360
452
  return () => h$1(clonedComponent, { ...props && typeof props === "object" ? props : {}, ...attrs }, slots);
361
453
  }
@@ -29,41 +29,41 @@ interface TestOptions {
29
29
  fixture: string;
30
30
  /**
31
31
  * Name of the configuration file.
32
- * @default `'nuxt.config`
32
+ * @default 'nuxt.config'
33
33
  */
34
34
  configFile: string;
35
35
  /**
36
36
  * Path to a directory with a Nuxt app to be put under test.
37
- * @default `'.'`
37
+ * @default '.'
38
38
  */
39
39
  rootDir: string;
40
40
  buildDir: string;
41
41
  nuxtConfig: NuxtConfig;
42
42
  /**
43
43
  * Whether to run a separate build step.
44
- * @default `true` (`false` if `browser` or `server` is disabled, or if a `host` is provided)
44
+ * @default true // (`false` if `browser` or `server` is disabled, or if a `host` is provided)
45
45
  */
46
46
  build: boolean;
47
47
  dev: boolean;
48
48
  /**
49
49
  * The amount of time (in milliseconds) to allow for `setupTest` to complete its work (which could include building or generating files for a Nuxt application, depending on the options that are passed).
50
- * @default `120000` or `240000` on windows
50
+ * @default 120000 // or `240000` on windows
51
51
  */
52
52
  setupTimeout: number;
53
53
  /**
54
54
  * The amount of time (in milliseconds) to allow tearing down the test environment, such as closing the browser.
55
- * @default `30000`
55
+ * @default 30000
56
56
  */
57
57
  teardownTimeout: number;
58
58
  waitFor: number;
59
59
  /**
60
60
  * Under the hood, Nuxt test utils uses [`playwright`](https://playwright.dev) to carry out browser testing. If this option is set, a browser will be launched and can be controlled in the subsequent test suite.
61
- * @default `false`
61
+ * @default false
62
62
  */
63
63
  browser: boolean;
64
64
  /**
65
65
  * Specify the runner for the test suite. One of `'vitest' | 'jest' | 'cucumber' | 'bun'`.
66
- * @default `vitest`
66
+ * @default 'vitest'
67
67
  */
68
68
  runner: TestRunner;
69
69
  logLevel: number;
@@ -75,17 +75,17 @@ interface TestOptions {
75
75
  };
76
76
  /**
77
77
  * Whether to launch a server to respond to requests in the test suite.
78
- * @default `true` (`false` if a `host` is provided)
78
+ * @default true // (`false` if a `host` is provided)
79
79
  */
80
80
  server: boolean;
81
81
  /**
82
82
  * If provided, a URL to use as the test target instead of building and running a new server. Useful for running "real" end-to-end tests against a deployed version of your application, or against an already running local server.
83
- * @default `undefined`
83
+ * @default undefined
84
84
  */
85
85
  host?: string;
86
86
  /**
87
87
  * If provided, set the launched test server port to the value.
88
- * @default `undefined`
88
+ * @default undefined
89
89
  */
90
90
  port?: number;
91
91
  env?: StartServerOptions['env'];
@@ -109,7 +109,7 @@ interface TestHooks {
109
109
  afterAll: () => Promise<void>;
110
110
  beforeAll: () => Promise<void>;
111
111
  /**
112
- * @deprecated use `beforeAll` intead
112
+ * @deprecated use `beforeAll` instead
113
113
  */
114
114
  setup: () => Promise<void>;
115
115
  ctx: TestContext;
@@ -1,4 +1,4 @@
1
- import { u as useTestContext, d as url, c as createTestContext, a as startServer, b as stopServer, s as setTestContext } from './test-utils.B8qEdk9k.mjs';
1
+ import { u as useTestContext, d as url, c as createTestContext, a as startServer, b as stopServer, s as setTestContext } from './test-utils.CtwoJP76.mjs';
2
2
  import { existsSync, promises } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { defu } from 'defu';
@@ -59,7 +59,7 @@ async function waitForHydration(page, url2, waitUntil) {
59
59
 
60
60
  const kit = _kit.default || _kit;
61
61
  const isNuxtApp = (dir) => {
62
- return existsSync(dir) && (existsSync(resolve(dir, "pages")) || existsSync(resolve(dir, "nuxt.config.js")) || existsSync(resolve(dir, "nuxt.config.mjs")) || existsSync(resolve(dir, "nuxt.config.cjs")) || existsSync(resolve(dir, "nuxt.config.ts")));
62
+ return existsSync(dir) && (existsSync(resolve(dir, "pages")) || existsSync(resolve(dir, "nuxt.config.js")) || existsSync(resolve(dir, "nuxt.config.mjs")) || existsSync(resolve(dir, "nuxt.config.cjs")) || existsSync(resolve(dir, "nuxt.config.ts")) || existsSync(resolve(dir, ".config", "nuxt.js")) || existsSync(resolve(dir, ".config", "nuxt.mjs")) || existsSync(resolve(dir, ".config", "nuxt.cjs")) || existsSync(resolve(dir, ".config", "nuxt.ts")));
63
63
  };
64
64
  const resolveRootDir = () => {
65
65
  const { options } = useTestContext();
@@ -14,7 +14,7 @@ function createTestContext(options) {
14
14
  fixture: "fixture",
15
15
  configFile: "nuxt.config",
16
16
  setupTimeout: isWindows ? 24e4 : 12e4,
17
- teardownTimeout: 3e4,
17
+ teardownTimeout: isWindows ? 6e4 : 3e4,
18
18
  dev: !!JSON.parse(process.env.NUXT_TEST_DEV || "false"),
19
19
  logLevel: 1,
20
20
  server: true,
@@ -41,26 +41,51 @@ async function setupWindow(win, environmentOptions) {
41
41
  app.id = rootId;
42
42
  win.document.body.appendChild(app);
43
43
  const h3App = createApp();
44
- if (!win.fetch) {
44
+ if (!win.fetch || !("Request" in win)) {
45
45
  await import('node-fetch-native/polyfill');
46
46
  win.URLSearchParams = globalThis.URLSearchParams;
47
+ win.Request ??= class Request extends globalThis.Request {
48
+ constructor(input, init) {
49
+ if (typeof input === "string") {
50
+ super(new URL(input, win.location.origin), init);
51
+ } else {
52
+ super(input, init);
53
+ }
54
+ }
55
+ };
47
56
  }
48
57
  const nodeHandler = toNodeListener(h3App);
49
58
  const registry = /* @__PURE__ */ new Set();
50
- win.fetch = async (url, init) => {
51
- if (typeof url === "string") {
52
- const base = url.split("?")[0];
53
- if (registry.has(base) || registry.has(url)) {
54
- url = "/_" + url;
55
- }
56
- if (url.startsWith("/")) {
57
- const response = await fetchNodeRequestHandler(nodeHandler, url, init);
58
- return normalizeFetchResponse(response);
59
- }
59
+ const _fetch = fetch;
60
+ win.fetch = async (input, _init) => {
61
+ let url;
62
+ let init = _init;
63
+ if (typeof input === "string") {
64
+ url = input;
65
+ } else if (input instanceof URL) {
66
+ url = input.toString();
67
+ } else {
68
+ url = input.url;
69
+ init = {
70
+ method: init?.method ?? input.method,
71
+ body: init?.body ?? input.body,
72
+ headers: init?.headers ?? input.headers
73
+ };
74
+ }
75
+ const base = url.split("?")[0];
76
+ if (registry.has(base) || registry.has(url)) {
77
+ url = "/_" + url;
60
78
  }
61
- return fetch(url, init);
79
+ if (url.startsWith("/")) {
80
+ const response = await fetchNodeRequestHandler(nodeHandler, url, init);
81
+ return normalizeFetchResponse(response);
82
+ }
83
+ return _fetch(input, _init);
62
84
  };
63
85
  win.$fetch = createFetch({ fetch: win.fetch, Headers: win.Headers });
86
+ win.$fetch.create = (options = {}) => {
87
+ return createFetch({ fetch: win.fetch, Headers: win.Headers, ...options });
88
+ };
64
89
  win.__registry = registry;
65
90
  win.__app = h3App;
66
91
  const timestamp = Date.now();
@@ -150,10 +175,11 @@ const jsdom = (async function(global, { jsdom = {} }) {
150
175
  console: false,
151
176
  cookieJar: false
152
177
  });
178
+ const virtualConsole = jsdomOptions.console && global.console ? new VirtualConsole() : void 0;
153
179
  const window = new JSDOM(jsdomOptions.html, {
154
180
  ...jsdomOptions,
155
181
  resources: jsdomOptions.resources ?? (jsdomOptions.userAgent ? new ResourceLoader({ userAgent: jsdomOptions.userAgent }) : void 0),
156
- virtualConsole: jsdomOptions.console && global.console ? new VirtualConsole().sendTo(global.console) : void 0,
182
+ virtualConsole: virtualConsole ? "sendTo" in virtualConsole ? virtualConsole.sendTo(global.console) : virtualConsole.forwardTo(global.console) : void 0,
157
183
  cookieJar: jsdomOptions.cookieJar ? new CookieJar() : void 0
158
184
  }).window;
159
185
  window.scrollTo = () => {
@@ -161,6 +187,7 @@ const jsdom = (async function(global, { jsdom = {} }) {
161
187
  return {
162
188
  window,
163
189
  teardown() {
190
+ window.close();
164
191
  }
165
192
  };
166
193
  });
@@ -184,21 +211,15 @@ const index = {
184
211
  jsdom: { url }
185
212
  }));
186
213
  if (environmentOptions?.nuxt?.mock?.intersectionObserver) {
187
- win.IntersectionObserver = win.IntersectionObserver || class IntersectionObserver {
188
- observe() {
189
- }
190
- unobserve() {
191
- }
192
- disconnect() {
193
- }
194
- };
214
+ win.IntersectionObserver ||= IntersectionObserver;
195
215
  }
196
216
  if (environmentOptions?.nuxt?.mock?.indexedDb) {
197
217
  win.indexedDB = indexedDB;
198
218
  }
199
219
  const teardownWindow = await setupWindow(win, environmentOptions);
200
220
  const { keys, originals } = populateGlobal(global, win, {
201
- bindFunctions: true
221
+ bindFunctions: true,
222
+ additionalKeys: ["fetch", "Request"]
202
223
  });
203
224
  return {
204
225
  // called after all tests with this env have been run
@@ -206,10 +227,24 @@ const index = {
206
227
  keys.forEach((key) => delete global[key]);
207
228
  teardownWindow();
208
229
  originals.forEach((v, k) => global[k] = v);
230
+ if (!global.IntersectionObserver) {
231
+ global.IntersectionObserver = IntersectionObserver;
232
+ }
209
233
  teardown();
210
234
  }
211
235
  };
212
236
  }
213
237
  };
238
+ class IntersectionObserver {
239
+ observe() {
240
+ }
241
+ unobserve() {
242
+ }
243
+ disconnect() {
244
+ }
245
+ takeRecords() {
246
+ return [];
247
+ }
248
+ }
214
249
 
215
250
  export { index as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxt/test-utils",
3
- "version": "3.19.1",
3
+ "version": "3.20.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/nuxt/test-utils.git"
@@ -56,81 +56,85 @@
56
56
  "scripts": {
57
57
  "lint": "eslint .",
58
58
  "lint:fix": "eslint . --fix",
59
+ "test": "pnpm test:types && pnpm test:unit && pnpm test:examples",
59
60
  "test:examples": "pnpm --filter '!example-app-cucumber' --filter '!example-app-jest' -r test && pnpm --filter example-app-cucumber -r test",
61
+ "test:knip": "knip",
62
+ "test:engines": "pnpm installed-check --no-workspaces --ignore-dev",
60
63
  "test:types": "vue-tsc --noEmit",
61
64
  "test:unit": "vitest test/unit --run",
65
+ "build": "unbuild",
62
66
  "prepack": "unbuild",
63
- "dev:prepare": "nuxi prepare && unbuild --stub && pnpm -r dev:prepare"
67
+ "dev:prepare": "nuxt prepare && unbuild --stub && pnpm -r dev:prepare"
64
68
  },
65
69
  "dependencies": {
66
- "@nuxt/kit": "^3.17.4",
67
- "@nuxt/schema": "^3.17.4",
68
- "c12": "^3.0.4",
70
+ "@nuxt/kit": "^4.1.3",
71
+ "c12": "^3.3.1",
69
72
  "consola": "^3.4.2",
70
73
  "defu": "^6.1.4",
71
74
  "destr": "^2.0.5",
72
75
  "estree-walker": "^3.0.3",
73
- "fake-indexeddb": "^6.0.1",
74
- "get-port-please": "^3.1.2",
75
- "h3": "^1.15.3",
76
- "local-pkg": "^1.1.1",
77
- "magic-string": "^0.30.17",
78
- "node-fetch-native": "^1.6.5",
79
- "node-mock-http": "^1.0.0",
76
+ "fake-indexeddb": "^6.2.4",
77
+ "get-port-please": "^3.2.0",
78
+ "h3": "^1.15.4",
79
+ "local-pkg": "^1.1.2",
80
+ "magic-string": "^0.30.19",
81
+ "node-fetch-native": "^1.6.7",
82
+ "node-mock-http": "^1.0.3",
80
83
  "ofetch": "^1.4.1",
81
84
  "pathe": "^2.0.3",
82
- "perfect-debounce": "^1.0.0",
85
+ "perfect-debounce": "^2.0.0",
83
86
  "radix3": "^1.1.2",
84
87
  "scule": "^1.3.0",
85
- "std-env": "^3.9.0",
88
+ "std-env": "^3.10.0",
86
89
  "tinyexec": "^1.0.1",
87
90
  "ufo": "^1.6.1",
88
- "unplugin": "^2.3.4",
89
- "vite": "^6.3.5",
91
+ "unplugin": "^2.3.10",
90
92
  "vitest-environment-nuxt": "^1.0.1",
91
- "vue": "^3.5.14"
93
+ "vue": "^3.5.22"
92
94
  },
93
95
  "devDependencies": {
94
- "@cucumber/cucumber": "11.3.0",
95
- "@jest/globals": "29.7.0",
96
- "@nuxt/devtools-kit": "2.4.1",
97
- "@nuxt/eslint-config": "1.4.0",
98
- "@playwright/test": "1.52.0",
96
+ "@cucumber/cucumber": "12.2.0",
97
+ "@jest/globals": "30.2.0",
98
+ "@nuxt/devtools-kit": "2.6.5",
99
+ "@nuxt/eslint-config": "1.9.0",
100
+ "@nuxt/schema": "4.1.3",
101
+ "@playwright/test": "1.56.1",
99
102
  "@testing-library/vue": "8.1.0",
100
- "@types/bun": "1.2.13",
101
- "@types/estree": "1.0.7",
102
- "@types/jsdom": "21.1.7",
103
- "@types/node": "22.15.19",
104
- "@types/semver": "7.7.0",
103
+ "@types/bun": "1.3.0",
104
+ "@types/estree": "1.0.8",
105
+ "@types/jsdom": "27.0.0",
106
+ "@types/node": "latest",
107
+ "@types/semver": "7.7.1",
105
108
  "@vue/test-utils": "2.4.6",
106
- "changelogen": "0.6.1",
109
+ "changelogen": "0.6.2",
107
110
  "compatx": "0.2.0",
108
- "eslint": "9.27.0",
111
+ "eslint": "9.38.0",
109
112
  "installed-check": "9.3.0",
110
- "knip": "5.56.0",
111
- "nitropack": "2.11.12",
112
- "nuxt": "3.17.4",
113
- "pkg-pr-new": "0.0.50",
114
- "playwright-core": "1.52.0",
115
- "rollup": "4.41.0",
116
- "semver": "7.7.2",
117
- "typescript": "5.8.3",
113
+ "knip": "5.66.2",
114
+ "nitropack": "2.12.7",
115
+ "nuxt": "4.1.3",
116
+ "pkg-pr-new": "0.0.60",
117
+ "playwright-core": "1.56.1",
118
+ "rollup": "4.52.5",
119
+ "semver": "7.7.3",
120
+ "typescript": "5.9.3",
118
121
  "unbuild": "latest",
119
- "unimport": "5.0.1",
120
- "vitest": "3.1.4",
121
- "vue-router": "4.5.1",
122
- "vue-tsc": "2.2.10"
122
+ "unimport": "5.5.0",
123
+ "vite": "7.1.11",
124
+ "vitest": "3.2.4",
125
+ "vue-router": "4.6.3",
126
+ "vue-tsc": "3.1.1"
123
127
  },
124
128
  "peerDependencies": {
125
- "@cucumber/cucumber": "^10.3.1 || ^11.0.0",
126
- "@jest/globals": "^29.5.0",
129
+ "@cucumber/cucumber": "^10.3.1 || >=11.0.0",
130
+ "@jest/globals": "^29.5.0 || >=30.0.0",
127
131
  "@playwright/test": "^1.43.1",
128
132
  "@testing-library/vue": "^7.0.0 || ^8.0.1",
129
133
  "@vue/test-utils": "^2.4.2",
130
- "happy-dom": "^9.10.9 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0",
131
- "jsdom": "^22.0.0 || ^23.0.0 || ^24.0.0 || ^25.0.0 || ^26.0.0",
134
+ "happy-dom": "*",
135
+ "jsdom": "*",
132
136
  "playwright-core": "^1.43.1",
133
- "vitest": "^0.34.6 || ^1.0.0 || ^2.0.0 || ^3.0.0"
137
+ "vitest": "^3.2.0"
134
138
  },
135
139
  "peerDependenciesMeta": {
136
140
  "@cucumber/cucumber": {
@@ -165,26 +169,31 @@
165
169
  }
166
170
  },
167
171
  "resolutions": {
168
- "@cucumber/cucumber": "11.3.0",
169
- "@nuxt/kit": "^3.17.4",
170
- "@nuxt/schema": "^3.17.4",
172
+ "@cucumber/cucumber": "12.2.0",
173
+ "@nuxt/kit": "^4.1.3",
174
+ "@nuxt/schema": "4.1.3",
171
175
  "@nuxt/test-utils": "workspace:*",
172
- "rollup": "4.41.0",
173
- "vite": "^6.3.5",
174
- "vite-node": "3.1.4",
175
- "vitest": "3.1.4",
176
- "vue": "^3.5.14"
176
+ "@types/node": "22.18.8",
177
+ "rollup": "4.52.5",
178
+ "vite": "7.1.11",
179
+ "vite-node": "3.2.4",
180
+ "vitest": "3.2.4",
181
+ "vue": "^3.5.22"
177
182
  },
178
183
  "engines": {
179
- "node": "^18.20.5 || ^20.9.0 || ^22.0.0 || >=23.0.0"
184
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
180
185
  },
181
- "packageManager": "pnpm@10.11.0",
186
+ "packageManager": "pnpm@10.18.3",
182
187
  "pnpm": {
183
188
  "onlyBuiltDependencies": [
189
+ "@tailwindcss/oxide",
184
190
  "better-sqlite3"
185
191
  ],
186
192
  "ignoredBuiltDependencies": [
187
- "esbuild"
193
+ "esbuild",
194
+ "oxc-resolver",
195
+ "unrs-resolver",
196
+ "vue-demi"
188
197
  ]
189
198
  }
190
199
  }