@nuxt/test-utils 3.19.2 → 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/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
@@ -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
  }
@@ -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';
@@ -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';
@@ -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,
@@ -130,20 +177,11 @@ async function mountSuspended(component, options) {
130
177
  async setup() {
131
178
  const router = useRouter();
132
179
  await router.replace(route);
133
- let interceptedEmit = null;
134
180
  const clonedComponent = {
135
181
  name: "MountSuspendedComponent",
136
182
  ...component,
137
183
  render: render ? function(_ctx, ...args) {
138
- const currentInstance = getCurrentInstance();
139
- if (currentInstance && currentInstance.emit !== interceptedEmit) {
140
- const oldEmit = currentInstance.emit;
141
- interceptedEmit = (event, ...args2) => {
142
- oldEmit(event, ...args2);
143
- setupContext.emit(event, ...args2);
144
- };
145
- currentInstance.emit = interceptedEmit;
146
- }
184
+ interceptEmitOnCurrentInstance();
147
185
  if (data && typeof data === "function") {
148
186
  const dataObject = data();
149
187
  for (const key in dataObject) {
@@ -187,7 +225,7 @@ async function mountSuspended(component, options) {
187
225
  }
188
226
  return render.call(this, renderContext, ...args);
189
227
  } : void 0,
190
- setup: setup ? (props2) => wrappedSetup(props2, setupContext) : void 0
228
+ setup: (props2) => wrappedSetup(props2, setupContext)
191
229
  };
192
230
  return () => h$1(clonedComponent, { ...props, ...setProps, ...attrs }, slots);
193
231
  }
@@ -232,6 +270,9 @@ function wrappedMountedWrapper(wrapper) {
232
270
  if (prop === "element") {
233
271
  const component = target.findComponent({ name: "MountSuspendedComponent" });
234
272
  return component[prop];
273
+ } else if (prop === "vm") {
274
+ const vm = Reflect.get(target, prop, receiver);
275
+ return createVMProxy(vm, wrapper.setupState);
235
276
  } else {
236
277
  return Reflect.get(target, prop, receiver);
237
278
  }
@@ -247,6 +288,28 @@ function wrappedMountedWrapper(wrapper) {
247
288
  }
248
289
  return proxy;
249
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
+ }
250
313
 
251
314
  const WRAPPER_EL_ID = "test-wrapper";
252
315
  async function renderSuspended(component, options) {
@@ -262,12 +325,30 @@ async function renderSuspended(component, options) {
262
325
  const { render, setup, data, computed, methods } = component;
263
326
  let setupContext;
264
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
+ }
265
345
  for (const fn of window.__cleanup || []) {
266
346
  fn();
267
347
  }
268
348
  document.querySelector(`#${WRAPPER_EL_ID}`)?.remove();
269
349
  let passedProps;
270
350
  const wrappedSetup = async (props2, setupContext2) => {
351
+ interceptEmitOnCurrentInstance();
271
352
  passedProps = props2;
272
353
  if (setup) {
273
354
  const result = await setup(props2, setupContext2);
@@ -322,6 +403,7 @@ async function renderSuspended(component, options) {
322
403
  name: "RenderSuspendedComponent",
323
404
  ...component,
324
405
  render: render ? function(_ctx, ...args) {
406
+ interceptEmitOnCurrentInstance();
325
407
  if (data && typeof data === "function") {
326
408
  const dataObject = data();
327
409
  for (const key in dataObject) {
@@ -365,7 +447,7 @@ async function renderSuspended(component, options) {
365
447
  }
366
448
  return render.call(this, renderContext, ...args);
367
449
  } : void 0,
368
- setup: setup ? (props2) => wrappedSetup(props2, setupContext) : void 0
450
+ setup: (props2) => wrappedSetup(props2, setupContext)
369
451
  };
370
452
  return () => h$1(clonedComponent, { ...props && typeof props === "object" ? props : {}, ...attrs }, slots);
371
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.2",
3
+ "version": "3.20.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/nuxt/test-utils.git"
@@ -56,6 +56,7 @@
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",
60
61
  "test:knip": "knip",
61
62
  "test:engines": "pnpm installed-check --no-workspaces --ignore-dev",
@@ -66,72 +67,72 @@
66
67
  "dev:prepare": "nuxt prepare && unbuild --stub && pnpm -r dev:prepare"
67
68
  },
68
69
  "dependencies": {
69
- "@nuxt/kit": "^3.17.5",
70
- "c12": "^3.0.4",
70
+ "@nuxt/kit": "^4.1.3",
71
+ "c12": "^3.3.1",
71
72
  "consola": "^3.4.2",
72
73
  "defu": "^6.1.4",
73
74
  "destr": "^2.0.5",
74
75
  "estree-walker": "^3.0.3",
75
- "fake-indexeddb": "^6.0.1",
76
- "get-port-please": "^3.1.2",
77
- "h3": "^1.15.3",
78
- "local-pkg": "^1.1.1",
79
- "magic-string": "^0.30.17",
80
- "node-fetch-native": "^1.6.5",
81
- "node-mock-http": "^1.0.1",
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",
82
83
  "ofetch": "^1.4.1",
83
84
  "pathe": "^2.0.3",
84
- "perfect-debounce": "^1.0.0",
85
+ "perfect-debounce": "^2.0.0",
85
86
  "radix3": "^1.1.2",
86
87
  "scule": "^1.3.0",
87
- "std-env": "^3.9.0",
88
+ "std-env": "^3.10.0",
88
89
  "tinyexec": "^1.0.1",
89
90
  "ufo": "^1.6.1",
90
- "unplugin": "^2.3.5",
91
+ "unplugin": "^2.3.10",
91
92
  "vitest-environment-nuxt": "^1.0.1",
92
- "vue": "^3.5.17"
93
+ "vue": "^3.5.22"
93
94
  },
94
95
  "devDependencies": {
95
- "@cucumber/cucumber": "11.3.0",
96
- "@jest/globals": "30.0.3",
97
- "@nuxt/devtools-kit": "2.6.0",
98
- "@nuxt/eslint-config": "1.4.1",
99
- "@nuxt/schema": "3.17.5",
100
- "@playwright/test": "1.53.2",
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",
101
102
  "@testing-library/vue": "8.1.0",
102
- "@types/bun": "1.2.17",
103
+ "@types/bun": "1.3.0",
103
104
  "@types/estree": "1.0.8",
104
- "@types/jsdom": "21.1.7",
105
+ "@types/jsdom": "27.0.0",
105
106
  "@types/node": "latest",
106
- "@types/semver": "7.7.0",
107
+ "@types/semver": "7.7.1",
107
108
  "@vue/test-utils": "2.4.6",
108
- "changelogen": "0.6.1",
109
+ "changelogen": "0.6.2",
109
110
  "compatx": "0.2.0",
110
- "eslint": "9.30.0",
111
+ "eslint": "9.38.0",
111
112
  "installed-check": "9.3.0",
112
- "knip": "5.61.3",
113
- "nitropack": "2.11.13",
114
- "nuxt": "3.17.5",
115
- "pkg-pr-new": "0.0.54",
116
- "playwright-core": "1.53.2",
117
- "rollup": "4.44.1",
118
- "semver": "7.7.2",
119
- "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",
120
121
  "unbuild": "latest",
121
- "unimport": "5.1.0",
122
- "vite": "7.0.0",
122
+ "unimport": "5.5.0",
123
+ "vite": "7.1.11",
123
124
  "vitest": "3.2.4",
124
- "vue-router": "4.5.1",
125
- "vue-tsc": "2.2.10"
125
+ "vue-router": "4.6.3",
126
+ "vue-tsc": "3.1.1"
126
127
  },
127
128
  "peerDependencies": {
128
- "@cucumber/cucumber": "^10.3.1 || ^11.0.0",
129
- "@jest/globals": "^29.5.0 || ^30.0.0",
129
+ "@cucumber/cucumber": "^10.3.1 || >=11.0.0",
130
+ "@jest/globals": "^29.5.0 || >=30.0.0",
130
131
  "@playwright/test": "^1.43.1",
131
132
  "@testing-library/vue": "^7.0.0 || ^8.0.1",
132
133
  "@vue/test-utils": "^2.4.2",
133
- "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 || ^18.0.0",
134
- "jsdom": "^22.0.0 || ^23.0.0 || ^24.0.0 || ^25.0.0 || ^26.0.0",
134
+ "happy-dom": "*",
135
+ "jsdom": "*",
135
136
  "playwright-core": "^1.43.1",
136
137
  "vitest": "^3.2.0"
137
138
  },
@@ -168,27 +169,31 @@
168
169
  }
169
170
  },
170
171
  "resolutions": {
171
- "@cucumber/cucumber": "11.3.0",
172
- "@nuxt/kit": "^3.17.5",
173
- "@nuxt/schema": "^3.17.5",
172
+ "@cucumber/cucumber": "12.2.0",
173
+ "@nuxt/kit": "^4.1.3",
174
+ "@nuxt/schema": "4.1.3",
174
175
  "@nuxt/test-utils": "workspace:*",
175
- "@types/node": "22.15.34",
176
- "rollup": "4.44.1",
177
- "vite": "7.0.0",
176
+ "@types/node": "22.18.8",
177
+ "rollup": "4.52.5",
178
+ "vite": "7.1.11",
178
179
  "vite-node": "3.2.4",
179
180
  "vitest": "3.2.4",
180
- "vue": "^3.5.17"
181
+ "vue": "^3.5.22"
181
182
  },
182
183
  "engines": {
183
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
184
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
184
185
  },
185
- "packageManager": "pnpm@10.12.4",
186
+ "packageManager": "pnpm@10.18.3",
186
187
  "pnpm": {
187
188
  "onlyBuiltDependencies": [
189
+ "@tailwindcss/oxide",
188
190
  "better-sqlite3"
189
191
  ],
190
192
  "ignoredBuiltDependencies": [
191
- "esbuild"
193
+ "esbuild",
194
+ "oxc-resolver",
195
+ "unrs-resolver",
196
+ "vue-demi"
192
197
  ]
193
198
  }
194
199
  }