@nuxt/test-utils 4.0.2 → 4.0.3

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.
@@ -0,0 +1,221 @@
1
+ import { reactive, h as h$1, Suspense, nextTick, getCurrentInstance, onErrorCaptured, effectScope } from 'vue';
2
+ import { defineComponent, h, tryUseNuxtApp, useRouter } from '#imports';
3
+ import NuxtRoot from '#build/root-component.mjs';
4
+ import { useLink } from 'vue-router';
5
+
6
+ const RouterLink = defineComponent({
7
+ functional: true,
8
+ props: {
9
+ to: {
10
+ type: [String, Object],
11
+ required: true
12
+ },
13
+ custom: Boolean,
14
+ replace: Boolean,
15
+ // Not implemented
16
+ activeClass: String,
17
+ exactActiveClass: String,
18
+ ariaCurrentValue: String
19
+ },
20
+ setup: (props, { slots }) => {
21
+ const link = useLink(props);
22
+ return () => {
23
+ const route = link.route.value;
24
+ const href = link.href.value;
25
+ const isActive = link.isActive.value;
26
+ const isExactActive = link.isExactActive.value;
27
+ return props.custom ? slots.default?.({ href, navigate: link.navigate, route, isActive, isExactActive }) : h(
28
+ "a",
29
+ {
30
+ href,
31
+ onClick: (e) => {
32
+ e.preventDefault();
33
+ return link.navigate(e);
34
+ }
35
+ },
36
+ slots
37
+ );
38
+ };
39
+ }
40
+ });
41
+
42
+ function cleanupAll() {
43
+ for (const fn of (window.__cleanup || []).splice(0)) {
44
+ fn();
45
+ }
46
+ }
47
+ function addCleanup(fn) {
48
+ window.__cleanup ||= [];
49
+ window.__cleanup.push(fn);
50
+ }
51
+ function runEffectScope(fn) {
52
+ const scope = effectScope();
53
+ addCleanup(() => scope.stop());
54
+ return scope.run(fn);
55
+ }
56
+ function wrapperSuspended(component, options, {
57
+ wrapperFn,
58
+ wrappedRender = (fn) => fn,
59
+ suspendedHelperName,
60
+ clonedComponentName
61
+ }) {
62
+ const { props = {}, attrs = {} } = options;
63
+ const { route = "/", scoped = false, ...wrapperFnOptions } = options;
64
+ const vueApp = tryUseNuxtApp()?.vueApp || globalThis.__unctx__.get("nuxt-app").tryUse().vueApp;
65
+ const {
66
+ render: componentRender,
67
+ setup: componentSetup,
68
+ ...componentRest
69
+ } = component;
70
+ let wrappedInstance = null;
71
+ let setupContext;
72
+ let setupState;
73
+ const setProps = reactive({});
74
+ function patchInstanceAppContext() {
75
+ const app = getCurrentInstance()?.appContext.app;
76
+ if (!app) return;
77
+ for (const [key, value] of Object.entries(vueApp)) {
78
+ if (key in app) continue;
79
+ app[key] = value;
80
+ }
81
+ }
82
+ const ClonedComponent = {
83
+ components: {},
84
+ ...component,
85
+ name: clonedComponentName,
86
+ async setup(props2, instanceContext) {
87
+ const currentInstance = getCurrentInstance();
88
+ if (currentInstance) {
89
+ currentInstance.emit = (event, ...args) => {
90
+ setupContext.emit(event, ...args);
91
+ };
92
+ }
93
+ if (!componentSetup) return;
94
+ const result = scoped ? await runEffectScope(() => componentSetup(props2, setupContext)) : await componentSetup(props2, setupContext);
95
+ if (wrappedInstance?.exposed) {
96
+ instanceContext.expose(wrappedInstance.exposed);
97
+ }
98
+ setupState = result && typeof result === "object" ? result : {};
99
+ return result;
100
+ }
101
+ };
102
+ const SuspendedHelper = {
103
+ name: suspendedHelperName,
104
+ render: () => "",
105
+ async setup() {
106
+ if (route) {
107
+ const router = useRouter();
108
+ await router.replace(route);
109
+ }
110
+ return () => h$1(ClonedComponent, { ...props, ...setProps, ...attrs }, setupContext.slots);
111
+ }
112
+ };
113
+ return new Promise((resolve, reject) => {
114
+ let isMountSettled = false;
115
+ const wrapper = wrapperFn(
116
+ {
117
+ inheritAttrs: false,
118
+ __cssModules: componentRest.__cssModules,
119
+ setup: (props2, ctx) => {
120
+ patchInstanceAppContext();
121
+ wrappedInstance = getCurrentInstance();
122
+ setupContext = ctx;
123
+ const nuxtRootSetupResult = runEffectScope(
124
+ () => NuxtRoot.setup(props2, {
125
+ ...ctx,
126
+ expose: () => {
127
+ }
128
+ })
129
+ );
130
+ onErrorCaptured((error, ...args) => {
131
+ if (isMountSettled) return;
132
+ isMountSettled = true;
133
+ try {
134
+ wrappedInstance?.appContext.config.errorHandler?.(error, ...args);
135
+ reject(error);
136
+ } catch (error2) {
137
+ reject(error2);
138
+ }
139
+ return false;
140
+ });
141
+ return nuxtRootSetupResult;
142
+ },
143
+ render: wrappedRender(() => h$1(
144
+ Suspense,
145
+ {
146
+ onResolve: () => nextTick().then(() => {
147
+ if (isMountSettled) return;
148
+ isMountSettled = true;
149
+ wrapper.setupState = setupState;
150
+ resolve({
151
+ wrapper,
152
+ setProps: (props2) => {
153
+ Object.assign(setProps, props2);
154
+ }
155
+ });
156
+ })
157
+ },
158
+ {
159
+ default: () => h$1(SuspendedHelper)
160
+ }
161
+ ))
162
+ },
163
+ {
164
+ ...wrapperFnOptions,
165
+ global: mergeComponentMountingGlobalOptions(wrapperFnOptions.global, {
166
+ config: {
167
+ globalProperties: makeAllPropertiesEnumerable(
168
+ vueApp.config.globalProperties
169
+ )
170
+ },
171
+ directives: vueApp._context.directives,
172
+ provide: vueApp._context.provides,
173
+ stubs: {
174
+ Suspense: false,
175
+ [SuspendedHelper.name]: false,
176
+ [ClonedComponent.name]: false
177
+ },
178
+ components: { ...vueApp._context.components, RouterLink }
179
+ })
180
+ }
181
+ );
182
+ });
183
+ }
184
+ function mergeComponentMountingGlobalOptions(options = {}, defaults = {}) {
185
+ const compilerOptions = {
186
+ ...defaults.config?.compilerOptions,
187
+ ...options.config?.compilerOptions
188
+ };
189
+ return {
190
+ ...options,
191
+ mixins: [...defaults.mixins || [], ...options.mixins || []],
192
+ stubs: {
193
+ ...defaults.stubs,
194
+ ...Array.isArray(options.stubs) ? Object.fromEntries(options.stubs.map((n) => [n, true])) : options.stubs
195
+ },
196
+ plugins: [...defaults.plugins || [], ...options.plugins || []],
197
+ components: { ...defaults.components, ...options.components },
198
+ provide: { ...defaults.provide, ...options.provide },
199
+ mocks: { ...defaults.mocks, ...options.mocks },
200
+ config: {
201
+ ...defaults.config,
202
+ ...options.config,
203
+ ...Object.keys(compilerOptions).length ? { compilerOptions } : void 0,
204
+ globalProperties: {
205
+ ...defaults.config?.globalProperties,
206
+ ...options.config?.globalProperties
207
+ }
208
+ },
209
+ directives: { ...defaults.directives, ...options.directives }
210
+ };
211
+ }
212
+ function makeAllPropertiesEnumerable(target) {
213
+ return {
214
+ ...target,
215
+ ...Object.fromEntries(
216
+ Object.getOwnPropertyNames(target).map((key) => [key, target[key]])
217
+ )
218
+ };
219
+ }
220
+
221
+ export { cleanupAll, wrapperSuspended };
package/dist/config.mjs CHANGED
@@ -290,9 +290,22 @@ function defineVitestConfig(config = {}) {
290
290
  return resolvedConfig;
291
291
  });
292
292
  }
293
+ function isCoverageEnabled(config) {
294
+ if (config.test && "coverage" in config.test && config.test.coverage?.enabled) {
295
+ return true;
296
+ }
297
+ return process.argv.some((arg) => arg === "--coverage" || arg === "--coverage.enabled" || arg === "--coverage=true" || arg === "--coverage.enabled=true");
298
+ }
293
299
  async function resolveConfig(config) {
294
300
  const overrides = config.test?.environmentOptions?.nuxt?.overrides || {};
295
301
  overrides.rootDir = config.test?.environmentOptions?.nuxt?.rootDir;
302
+ if (isCoverageEnabled(config)) {
303
+ if (overrides.sourcemap === void 0) {
304
+ overrides.sourcemap = { client: true };
305
+ } else if (typeof overrides.sourcemap === "object" && overrides.sourcemap.client === void 0) {
306
+ overrides.sourcemap.client = true;
307
+ }
308
+ }
296
309
  if (config.test?.setupFiles && !Array.isArray(config.test.setupFiles)) {
297
310
  config.test.setupFiles = [config.test.setupFiles].filter(Boolean);
298
311
  }
package/dist/e2e.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { T as TestOptions, b as TestContext, a as TestHooks } from './shared/test-utils.BLyxqr96.mjs';
2
- export { $ as $fetch, G as GotoOptions, N as NuxtPage, S as StartServerOptions, c as TestRunner, d as createBrowser, e as createPage, f as fetch, g as getBrowser, s as startServer, h as stopServer, u as url, w as waitForHydration } from './shared/test-utils.BLyxqr96.mjs';
1
+ import { T as TestOptions, b as TestContext, a as TestHooks } from './shared/test-utils.BX46zKiB.mjs';
2
+ export { $ as $fetch, G as GotoOptions, N as NuxtPage, S as StartServerOptions, c as TestRunner, d as createBrowser, e as createPage, f as fetch, g as getBrowser, s as startServer, h as stopServer, u as url, w as waitForHydration } from './shared/test-utils.BX46zKiB.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, s as setup, e as setupMaps, w as waitForHydration } from './shared/test-utils.E_cAGA8l.mjs';
2
- import { u as useTestContext } from './shared/test-utils.BsmyE2FA.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.BsmyE2FA.mjs';
1
+ export { b as buildFixture, c as createBrowser, a as createPage, d as createTest, g as getBrowser, l as loadFixture, s as setup, e as setupMaps, w as waitForHydration } from './shared/test-utils.RVsKJA_7.mjs';
2
+ import { u as useTestContext } from './shared/test-utils.BrQgu3Ob.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.BrQgu3Ob.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.BsmyE2FA.mjs';
3
+ import { $ as $fetch, u as useTestContext } from './shared/test-utils.BrQgu3Ob.mjs';
4
4
  import 'tinyexec';
5
5
  import 'get-port-please';
6
6
  import 'ofetch';
package/dist/module.mjs CHANGED
@@ -263,6 +263,9 @@ function endOf(node) {
263
263
  return "range" in node && node.range ? node.range[1] : "end" in node ? node.end : void 0;
264
264
  }
265
265
 
266
+ function isTestPluginFile(src) {
267
+ return src.includes(".spec.") || src.includes(".test.");
268
+ }
266
269
  async function setupImportMocking(nuxt) {
267
270
  const { addVitePlugin } = await loadKit(nuxt.options.rootDir);
268
271
  const ctx = {
@@ -291,6 +294,9 @@ async function setupImportMocking(nuxt) {
291
294
  nuxt._ignore.add(`!${pattern}`);
292
295
  }
293
296
  }
297
+ nuxt.hook("app:resolve", (app) => {
298
+ app.plugins = app.plugins.filter((plugin) => !isTestPluginFile(plugin.src));
299
+ });
294
300
  addVitePlugin(createMockPlugin(ctx).vite());
295
301
  }
296
302
 
@@ -952,7 +958,7 @@ function vitestWrapper(options) {
952
958
  };
953
959
  }
954
960
 
955
- const version = "4.0.2";
961
+ const version = "4.0.3";
956
962
  const pkg = {
957
963
  version: version};
958
964
 
@@ -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, a as TestHooks } from './shared/test-utils.BLyxqr96.mjs';
4
+ import { T as TestOptions$1, G as GotoOptions, a as TestHooks } from './shared/test-utils.BX46zKiB.mjs';
5
5
  import '@nuxt/schema';
6
6
  import 'tinyexec';
7
7
  import 'ofetch';
@@ -2,7 +2,7 @@ import defu from 'defu';
2
2
  import { test as test$1 } from '@playwright/test';
3
3
  export { expect } from '@playwright/test';
4
4
  import { isWindows } from 'std-env';
5
- import { w as waitForHydration, d as createTest } from './shared/test-utils.E_cAGA8l.mjs';
5
+ import { w as waitForHydration, d as createTest } from './shared/test-utils.RVsKJA_7.mjs';
6
6
  import 'node:path';
7
7
  import 'ufo';
8
8
  import 'consola';
@@ -11,7 +11,7 @@ import 'destr';
11
11
  import 'scule';
12
12
  import 'node:url';
13
13
  import 'exsolve';
14
- import { d as url } from './shared/test-utils.BsmyE2FA.mjs';
14
+ import { d as url } from './shared/test-utils.BrQgu3Ob.mjs';
15
15
  import 'pathe';
16
16
  import '#dirs';
17
17
  import './shared/test-utils.BIY9XRkB.mjs';
@@ -1,7 +1,5 @@
1
1
  import { mount } from '@vue/test-utils';
2
- import { reactive, h as h$1, Suspense, nextTick, getCurrentInstance, onErrorCaptured, effectScope } from 'vue';
3
- import { defineComponent, useRouter, h, tryUseNuxtApp } from '#imports';
4
- import NuxtRoot from '#build/root-component.mjs';
2
+ import { h, nextTick } from 'vue';
5
3
 
6
4
  function getEndpointRegistry() {
7
5
  const app = window.__app ?? {};
@@ -75,219 +73,8 @@ function registerGlobalHandler(app) {
75
73
  return true;
76
74
  }
77
75
 
78
- const RouterLink = defineComponent({
79
- functional: true,
80
- props: {
81
- to: {
82
- type: [String, Object],
83
- required: true
84
- },
85
- custom: Boolean,
86
- replace: Boolean,
87
- // Not implemented
88
- activeClass: String,
89
- exactActiveClass: String,
90
- ariaCurrentValue: String
91
- },
92
- setup: (props, { slots }) => {
93
- const navigate = () => {
94
- };
95
- return () => {
96
- const route = useRouter().resolve(props.to);
97
- return props.custom ? slots.default?.({ href: route.href, navigate, route }) : h(
98
- "a",
99
- {
100
- href: route.href,
101
- onClick: (e) => {
102
- e.preventDefault();
103
- return navigate();
104
- }
105
- },
106
- slots
107
- );
108
- };
109
- }
110
- });
111
-
112
- function cleanupAll() {
113
- for (const fn of (window.__cleanup || []).splice(0)) {
114
- fn();
115
- }
116
- }
117
- function addCleanup(fn) {
118
- window.__cleanup ||= [];
119
- window.__cleanup.push(fn);
120
- }
121
- function runEffectScope(fn) {
122
- const scope = effectScope();
123
- addCleanup(() => scope.stop());
124
- return scope.run(fn);
125
- }
126
- function wrapperSuspended(component, options, {
127
- wrapperFn,
128
- wrappedRender = (fn) => fn,
129
- suspendedHelperName,
130
- clonedComponentName
131
- }) {
132
- const { props = {}, attrs = {} } = options;
133
- const { route = "/", scoped = false, ...wrapperFnOptions } = options;
134
- const vueApp = tryUseNuxtApp()?.vueApp || globalThis.__unctx__.get("nuxt-app").tryUse().vueApp;
135
- const {
136
- render: componentRender,
137
- setup: componentSetup,
138
- ...componentRest
139
- } = component;
140
- let wrappedInstance = null;
141
- let setupContext;
142
- let setupState;
143
- const setProps = reactive({});
144
- function patchInstanceAppContext() {
145
- const app = getCurrentInstance()?.appContext.app;
146
- if (!app) return;
147
- for (const [key, value] of Object.entries(vueApp)) {
148
- if (key in app) continue;
149
- app[key] = value;
150
- }
151
- }
152
- const ClonedComponent = {
153
- components: {},
154
- ...component,
155
- name: clonedComponentName,
156
- async setup(props2, instanceContext) {
157
- const currentInstance = getCurrentInstance();
158
- if (currentInstance) {
159
- currentInstance.emit = (event, ...args) => {
160
- setupContext.emit(event, ...args);
161
- };
162
- }
163
- if (!componentSetup) return;
164
- const result = scoped ? await runEffectScope(() => componentSetup(props2, setupContext)) : await componentSetup(props2, setupContext);
165
- if (wrappedInstance?.exposed) {
166
- instanceContext.expose(wrappedInstance.exposed);
167
- }
168
- setupState = result && typeof result === "object" ? result : {};
169
- return result;
170
- }
171
- };
172
- const SuspendedHelper = {
173
- name: suspendedHelperName,
174
- render: () => "",
175
- async setup() {
176
- if (route) {
177
- const router = useRouter();
178
- await router.replace(route);
179
- }
180
- return () => h$1(ClonedComponent, { ...props, ...setProps, ...attrs }, setupContext.slots);
181
- }
182
- };
183
- return new Promise((resolve, reject) => {
184
- let isMountSettled = false;
185
- const wrapper = wrapperFn(
186
- {
187
- inheritAttrs: false,
188
- __cssModules: componentRest.__cssModules,
189
- setup: (props2, ctx) => {
190
- patchInstanceAppContext();
191
- wrappedInstance = getCurrentInstance();
192
- setupContext = ctx;
193
- const nuxtRootSetupResult = runEffectScope(
194
- () => NuxtRoot.setup(props2, {
195
- ...ctx,
196
- expose: () => {
197
- }
198
- })
199
- );
200
- onErrorCaptured((error, ...args) => {
201
- if (isMountSettled) return;
202
- isMountSettled = true;
203
- try {
204
- wrappedInstance?.appContext.config.errorHandler?.(error, ...args);
205
- reject(error);
206
- } catch (error2) {
207
- reject(error2);
208
- }
209
- return false;
210
- });
211
- return nuxtRootSetupResult;
212
- },
213
- render: wrappedRender(() => h$1(
214
- Suspense,
215
- {
216
- onResolve: () => nextTick().then(() => {
217
- if (isMountSettled) return;
218
- isMountSettled = true;
219
- wrapper.setupState = setupState;
220
- resolve({
221
- wrapper,
222
- setProps: (props2) => {
223
- Object.assign(setProps, props2);
224
- }
225
- });
226
- })
227
- },
228
- {
229
- default: () => h$1(SuspendedHelper)
230
- }
231
- ))
232
- },
233
- {
234
- ...wrapperFnOptions,
235
- global: mergeComponentMountingGlobalOptions(wrapperFnOptions.global, {
236
- config: {
237
- globalProperties: makeAllPropertiesEnumerable(
238
- vueApp.config.globalProperties
239
- )
240
- },
241
- directives: vueApp._context.directives,
242
- provide: vueApp._context.provides,
243
- stubs: {
244
- Suspense: false,
245
- [SuspendedHelper.name]: false,
246
- [ClonedComponent.name]: false
247
- },
248
- components: { ...vueApp._context.components, RouterLink }
249
- })
250
- }
251
- );
252
- });
253
- }
254
- function mergeComponentMountingGlobalOptions(options = {}, defaults = {}) {
255
- return {
256
- ...options,
257
- mixins: [...defaults.mixins || [], ...options.mixins || []],
258
- stubs: {
259
- ...defaults.stubs,
260
- ...Array.isArray(options.stubs) ? Object.fromEntries(options.stubs.map((n) => [n, true])) : options.stubs
261
- },
262
- plugins: [...defaults.plugins || [], ...options.plugins || []],
263
- components: { ...defaults.components, ...options.components },
264
- provide: { ...defaults.provide, ...options.provide },
265
- mocks: { ...defaults.mocks, ...options.mocks },
266
- config: {
267
- ...defaults.config,
268
- ...options.config,
269
- compilerOptions: {
270
- ...defaults.config?.compilerOptions,
271
- ...options.config?.compilerOptions
272
- },
273
- globalProperties: {
274
- ...defaults.config?.globalProperties,
275
- ...options.config?.globalProperties
276
- }
277
- },
278
- directives: { ...defaults.directives, ...options.directives }
279
- };
280
- }
281
- function makeAllPropertiesEnumerable(target) {
282
- return {
283
- ...target,
284
- ...Object.fromEntries(
285
- Object.getOwnPropertyNames(target).map((key) => [key, target[key]])
286
- )
287
- };
288
- }
289
-
290
76
  async function mountSuspended(component, options = {}) {
77
+ const { cleanupAll, wrapperSuspended } = await import('../chunks/suspended.mjs');
291
78
  const suspendedHelperName = "MountSuspendedHelper";
292
79
  const clonedComponentName = "MountSuspendedComponent";
293
80
  cleanupAll();
@@ -342,6 +129,7 @@ function wrappedMountedWrapper(wrapper, component) {
342
129
  }
343
130
 
344
131
  async function renderSuspended(component, options = {}) {
132
+ const { cleanupAll, wrapperSuspended } = await import('../chunks/suspended.mjs');
345
133
  const wrapperId = "test-wrapper";
346
134
  const suspendedHelperName = "RenderHelper";
347
135
  const clonedComponentName = "RenderSuspendedComponent";
@@ -350,9 +138,9 @@ async function renderSuspended(component, options = {}) {
350
138
  document.getElementById(wrapperId)?.remove();
351
139
  const { wrapper, setProps } = await wrapperSuspended(component, options, {
352
140
  wrapperFn,
353
- wrappedRender: (render) => () => h$1({
141
+ wrappedRender: (render) => () => h({
354
142
  inheritAttrs: false,
355
- render: () => h$1("div", { id: wrapperId }, render())
143
+ render: () => h("div", { id: wrapperId }, render())
356
144
  }),
357
145
  suspendedHelperName,
358
146
  clonedComponentName
@@ -56,6 +56,13 @@ interface TestOptions {
56
56
  * @default 30000
57
57
  */
58
58
  teardownTimeout: number;
59
+ /**
60
+ * The amount of time (in milliseconds) to wait for the dev or built server to become ready (i.e. respond successfully on the configured base URL) before failing.
61
+ *
62
+ * This is bounded by `setupTimeout`, so increasing this is only useful in combination with a sufficiently large `setupTimeout`.
63
+ * @default 120000 // on windows; otherwise 60000
64
+ */
65
+ serverStartTimeout: number;
59
66
  waitFor: number;
60
67
  /**
61
68
  * 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.
@@ -15,6 +15,7 @@ function createTestContext(options) {
15
15
  configFile: "nuxt.config",
16
16
  setupTimeout: isWindows ? 24e4 : 12e4,
17
17
  teardownTimeout: isWindows ? 6e4 : 3e4,
18
+ serverStartTimeout: isWindows ? 12e4 : 6e4,
18
19
  dev: !!JSON.parse(process.env.NUXT_TEST_DEV || "false"),
19
20
  logLevel: 1,
20
21
  server: true,
@@ -97,24 +98,6 @@ async function startServer(options = {}) {
97
98
  }
98
99
  }
99
100
  });
100
- await waitForPort(port, { retries: 32, host }).catch(() => {
101
- });
102
- let lastError;
103
- for (let i = 0; i < 150; i++) {
104
- await new Promise((resolve2) => setTimeout(resolve2, 100));
105
- try {
106
- const res = await $fetch(ctx.nuxt.options.app.baseURL, {
107
- responseType: "text"
108
- });
109
- if (!res.includes("__NUXT_LOADING__")) {
110
- return;
111
- }
112
- } catch (e) {
113
- lastError = e;
114
- }
115
- }
116
- ctx.serverProcess.kill();
117
- throw lastError || new Error("Timeout waiting for dev server!");
118
101
  } else {
119
102
  const outputDir = ctx.nuxt ? ctx.nuxt.options.nitro.output.dir : ctx.options.nuxtConfig.nitro.output.dir;
120
103
  ctx.serverProcess = x(
@@ -135,13 +118,53 @@ async function startServer(options = {}) {
135
118
  }
136
119
  }
137
120
  );
138
- await waitForPort(port, { retries: 20, host });
139
121
  }
122
+ await waitForServer({ host, port, dev: ctx.options.dev });
123
+ }
124
+ async function waitForServer({ host, port, dev }) {
125
+ const ctx = useTestContext();
126
+ const baseURL = ctx.nuxt?.options.app.baseURL ?? "/";
127
+ const deadline = Date.now() + ctx.options.serverStartTimeout;
128
+ await waitForPort(port, { retries: 8, host }).catch(() => {
129
+ });
130
+ let lastError;
131
+ while (Date.now() < deadline) {
132
+ if (ctx.serverProcess && (ctx.serverProcess.killed || ctx.serverProcess.exitCode != null)) {
133
+ throw new Error(`Server process exited before becoming ready (exit code: ${ctx.serverProcess.exitCode ?? "unknown"})`);
134
+ }
135
+ try {
136
+ const res = await globalFetch(joinURL(ctx.url, baseURL), { signal: AbortSignal.timeout(1e4) });
137
+ if (dev && res.status === 503) {
138
+ lastError = new Error(`Server responded with ${res.status} ${res.statusText}`);
139
+ } else if (dev && (await res.text()).includes("__NUXT_LOADING__")) {
140
+ lastError = new Error("Dev server is still starting up");
141
+ } else {
142
+ return;
143
+ }
144
+ } catch (e) {
145
+ lastError = e;
146
+ }
147
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
148
+ }
149
+ await stopServer();
150
+ throw lastError instanceof Error ? lastError : new Error(`Timeout (${ctx.options.serverStartTimeout}ms) waiting for ${dev ? "dev" : "built"} server to become ready at ${ctx.url}`);
140
151
  }
141
152
  async function stopServer() {
142
153
  const ctx = useTestContext();
143
- if (ctx.serverProcess) {
144
- ctx.serverProcess.kill();
154
+ const proc = ctx.serverProcess;
155
+ if (!proc) {
156
+ return;
157
+ }
158
+ ctx.serverProcess = void 0;
159
+ const exited = Promise.resolve(proc).then(() => {
160
+ }, () => {
161
+ });
162
+ const sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
163
+ proc.kill();
164
+ await Promise.race([exited, sleep(5e3)]);
165
+ if (proc.exitCode == null) {
166
+ proc.kill("SIGKILL");
167
+ await Promise.race([exited, sleep(5e3)]);
145
168
  }
146
169
  }
147
170
  function fetch(path, options) {
@@ -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.BsmyE2FA.mjs';
1
+ import { u as useTestContext, d as url, c as createTestContext, a as startServer, b as stopServer, s as setTestContext } from './test-utils.BrQgu3Ob.mjs';
2
2
  import { existsSync, promises } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { defu } from 'defu';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxt/test-utils",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/nuxt/test-utils.git"
@@ -59,7 +59,7 @@
59
59
  "@nuxt/kit": "^3.21.2",
60
60
  "c12": "^3.3.4",
61
61
  "consola": "^3.4.2",
62
- "defu": "^6.1.4",
62
+ "defu": "^6.1.7",
63
63
  "destr": "^2.0.5",
64
64
  "estree-walker": "^3.0.3",
65
65
  "exsolve": "^1.0.8",
@@ -71,52 +71,52 @@
71
71
  "magic-string": "^0.30.21",
72
72
  "node-fetch-native": "^1.6.7",
73
73
  "node-mock-http": "^1.0.4",
74
- "nypm": "^0.6.5",
74
+ "nypm": "^0.6.6",
75
75
  "ofetch": "^1.5.1",
76
76
  "pathe": "^2.0.3",
77
77
  "perfect-debounce": "^2.1.0",
78
78
  "radix3": "^1.1.2",
79
79
  "scule": "^1.3.0",
80
- "std-env": "^4.0.0",
80
+ "std-env": "^4.1.0",
81
81
  "tinyexec": "^1.1.1",
82
82
  "ufo": "^1.6.3",
83
83
  "unplugin": "^3.0.0",
84
- "vue": "^3.5.32",
84
+ "vue": "^3.5.33",
85
85
  "vitest-environment-nuxt": "2.0.0"
86
86
  },
87
87
  "devDependencies": {
88
- "@cucumber/cucumber": "12.7.0",
88
+ "@cucumber/cucumber": "12.8.2",
89
89
  "@jest/globals": "30.3.0",
90
90
  "@nuxt/eslint-config": "1.15.2",
91
91
  "@nuxt/schema": "4.4.2",
92
92
  "@playwright/test": "1.59.1",
93
93
  "@testing-library/vue": "8.1.0",
94
- "@types/bun": "1.3.11",
94
+ "@types/bun": "1.3.13",
95
95
  "@types/estree": "1.0.8",
96
96
  "@types/jsdom": "28.0.1",
97
97
  "@types/node": "latest",
98
98
  "@types/semver": "7.7.1",
99
- "@vitest/browser-playwright": "4.1.2",
100
- "@vue/test-utils": "2.4.6",
99
+ "@vitest/browser-playwright": "4.1.5",
100
+ "@vue/test-utils": "2.4.9",
101
101
  "changelogen": "0.6.2",
102
102
  "compatx": "0.2.0",
103
- "eslint": "10.2.0",
103
+ "eslint": "10.2.1",
104
104
  "installed-check": "10.0.1",
105
- "knip": "6.3.0",
105
+ "knip": "6.7.0",
106
106
  "nitropack": "2.13.3",
107
107
  "nuxt": "4.4.2",
108
- "oxc-parser": "0.124.0",
109
- "pkg-pr-new": "0.0.66",
108
+ "oxc-parser": "0.127.0",
109
+ "pkg-pr-new": "0.0.67",
110
110
  "playwright-core": "1.59.1",
111
- "rollup": "4.60.1",
111
+ "rollup": "4.60.2",
112
112
  "semver": "7.7.4",
113
- "typescript": "6.0.2",
113
+ "typescript": "6.0.3",
114
114
  "unbuild": "latest",
115
- "unimport": "6.0.2",
116
- "vite": "8.0.5",
117
- "vitest": "4.1.2",
118
- "vue-router": "5.0.4",
119
- "vue-tsc": "3.2.6"
115
+ "unimport": "6.1.1",
116
+ "vite": "8.0.10",
117
+ "vitest": "4.1.5",
118
+ "vue-router": "5.0.6",
119
+ "vue-tsc": "3.2.7"
120
120
  },
121
121
  "peerDependencies": {
122
122
  "@cucumber/cucumber": ">=11.0.0",
@@ -162,16 +162,16 @@
162
162
  }
163
163
  },
164
164
  "resolutions": {
165
- "@cucumber/cucumber": "12.7.0",
165
+ "@cucumber/cucumber": "12.8.2",
166
166
  "@nuxt/schema": "4.4.2",
167
167
  "@nuxt/test-utils": "workspace:*",
168
168
  "@types/node": "24.12.2",
169
169
  "nitro": "https://pkg.pr.new/nitrojs/nitro@00598a8",
170
- "rollup": "4.60.1",
171
- "vite": "8.0.5",
170
+ "rollup": "4.60.2",
171
+ "vite": "8.0.10",
172
172
  "vite-node": "6.0.0",
173
- "vitest": "4.1.2",
174
- "vue": "^3.5.32"
173
+ "vitest": "4.1.5",
174
+ "vue": "^3.5.33"
175
175
  },
176
176
  "engines": {
177
177
  "node": "^20.19.0 || ^22.12.0 || >=24.0.0"