@nuxt/test-utils 3.20.1 → 3.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, effectScope, unref, isReadonly, getCurrentInstance, isRef, defineComponent as defineComponent$1 } from 'vue';
3
+ import { reactive, h as h$1, Suspense, nextTick as nextTick$1, getCurrentInstance, onErrorCaptured, effectScope } 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';
@@ -11,16 +11,25 @@ function registerEndpoint(url, options) {
11
11
  if (!app) {
12
12
  throw new Error("registerEndpoint() can only be used in a `@nuxt/test-utils` runtime environment");
13
13
  }
14
- const config = typeof options === "function" ? { handler: options, method: void 0 } : options;
14
+ const config = typeof options === "function" ? { handler: options, method: void 0, once: false } : options;
15
15
  config.handler = defineEventHandler(config.handler);
16
16
  const hasBeenRegistered = window.__registry.has(url);
17
17
  endpointRegistry[url] ||= [];
18
18
  endpointRegistry[url].push(config);
19
19
  if (!hasBeenRegistered) {
20
20
  window.__registry.add(url);
21
- app.use("/_" + url, defineEventHandler((event) => {
21
+ app.use("/_" + url, defineEventHandler(async (event) => {
22
22
  const latestHandler = [...endpointRegistry[url] || []].reverse().find((config2) => config2.method ? event.method === config2.method : true);
23
- return latestHandler?.handler(event);
23
+ if (!latestHandler) return;
24
+ const result = await latestHandler.handler(event);
25
+ if (!latestHandler.once) return result;
26
+ const index = endpointRegistry[url]?.indexOf(latestHandler);
27
+ if (index === void 0 || index === -1) return result;
28
+ endpointRegistry[url]?.splice(index, 1);
29
+ if (endpointRegistry[url]?.length === 0) {
30
+ window.__registry.delete(url);
31
+ }
32
+ return result;
24
33
  }), {
25
34
  match(_, event) {
26
35
  return endpointRegistry[url]?.some((config2) => config2.method ? event?.method === config2.method : true) ?? false;
@@ -34,7 +43,7 @@ function registerEndpoint(url, options) {
34
43
  }
35
44
  };
36
45
  }
37
- function mockNuxtImport(_name, _factory) {
46
+ function mockNuxtImport(_target, _factory) {
38
47
  throw new Error(
39
48
  "mockNuxtImport() is a macro and it did not get transpiled. This may be an internal bug of @nuxt/test-utils."
40
49
  );
@@ -79,406 +88,231 @@ const RouterLink = defineComponent({
79
88
  }
80
89
  });
81
90
 
82
- async function mountSuspended(component, options) {
83
- const {
84
- props = {},
85
- attrs = {},
86
- slots = {},
87
- route = "/",
88
- ..._options
89
- } = options || {};
90
- for (const cleanupFunction of globalThis.__cleanup || []) {
91
- cleanupFunction();
91
+ function cleanupAll() {
92
+ for (const fn of (window.__cleanup || []).splice(0)) {
93
+ fn();
92
94
  }
95
+ }
96
+ function addCleanup(fn) {
97
+ window.__cleanup ||= [];
98
+ window.__cleanup.push(fn);
99
+ }
100
+ function runEffectScope(fn) {
101
+ const scope = effectScope();
102
+ addCleanup(() => scope.stop());
103
+ return scope.run(fn);
104
+ }
105
+ function wrapperSuspended(component, options, {
106
+ wrapperFn,
107
+ wrappedRender = (fn) => fn,
108
+ suspendedHelperName,
109
+ clonedComponentName
110
+ }) {
111
+ const { props = {}, attrs = {} } = options;
112
+ const { route = "/", scoped = false, ...wrapperFnOptions } = options;
93
113
  const vueApp = tryUseNuxtApp()?.vueApp || globalThis.__unctx__.get("nuxt-app").tryUse().vueApp;
94
- const { render, setup, data, computed, methods } = component;
114
+ const {
115
+ render: componentRender,
116
+ setup: componentSetup,
117
+ ...componentRest
118
+ } = component;
119
+ let wrappedInstance = null;
95
120
  let setupContext;
96
121
  let setupState;
97
122
  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
- });
123
+ function patchInstanceAppContext() {
124
+ const app = getCurrentInstance()?.appContext.app;
125
+ if (!app) return;
126
+ for (const [key, value] of Object.entries(vueApp)) {
127
+ if (key in app) continue;
128
+ app[key] = value;
105
129
  }
106
- return interceptedEmit;
107
130
  }
108
- function interceptEmitOnCurrentInstance() {
109
- const currentInstance = getCurrentInstance();
110
- if (!currentInstance) {
111
- return;
112
- }
113
- currentInstance.emit = getInterceptedEmitFunction(currentInstance.emit);
114
- }
115
- let passedProps;
116
- let componentScope = null;
117
- const wrappedSetup = async (props2, setupContext2) => {
118
- interceptEmitOnCurrentInstance();
119
- passedProps = props2;
120
- if (setup) {
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);
131
+ const ClonedComponent = {
132
+ components: {},
133
+ ...component,
134
+ name: clonedComponentName,
135
+ async setup(props2, instanceContext) {
136
+ const currentInstance = getCurrentInstance();
137
+ if (currentInstance) {
138
+ currentInstance.emit = (event, ...args) => {
139
+ setupContext.emit(event, ...args);
140
+ };
141
+ }
142
+ if (!componentSetup) return;
143
+ const result = scoped ? await runEffectScope(() => componentSetup(props2, setupContext)) : await componentSetup(props2, setupContext);
144
+ if (wrappedInstance?.exposed) {
145
+ instanceContext.expose(wrappedInstance.exposed);
133
146
  }
134
147
  setupState = result && typeof result === "object" ? result : {};
135
148
  return result;
136
149
  }
137
150
  };
138
- return new Promise(
139
- (resolve) => {
140
- const vm = mount(
141
- {
142
- setup: (props2, ctx) => {
143
- setupContext = ctx;
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
- }
162
- },
163
- render: (renderContext) => h$1(
164
- Suspense,
165
- {
166
- onResolve: () => nextTick().then(() => {
167
- vm.setupState = setupState;
168
- vm.__setProps = (props2) => {
169
- Object.assign(setProps, props2);
170
- };
171
- resolve(wrappedMountedWrapper(vm));
172
- })
173
- },
174
- {
175
- default: () => h$1({
176
- name: "MountSuspendedHelper",
177
- async setup() {
178
- const router = useRouter();
179
- await router.replace(route);
180
- const clonedComponent = {
181
- name: "MountSuspendedComponent",
182
- ...component,
183
- render: render ? function(_ctx, ...args) {
184
- interceptEmitOnCurrentInstance();
185
- if (data && typeof data === "function") {
186
- const dataObject = data();
187
- for (const key in dataObject) {
188
- renderContext[key] = dataObject[key];
189
- }
190
- }
191
- for (const key in setupState || {}) {
192
- const warn = console.warn;
193
- console.warn = () => {
194
- };
195
- try {
196
- renderContext[key] = isReadonly(setupState[key]) ? unref(setupState[key]) : setupState[key];
197
- } catch {
198
- } finally {
199
- console.warn = warn;
200
- }
201
- if (key === "props") {
202
- renderContext[key] = cloneProps$1(renderContext[key]);
203
- }
204
- }
205
- const propsContext = "props" in renderContext ? renderContext.props : renderContext;
206
- for (const key in props || {}) {
207
- propsContext[key] = _ctx[key];
208
- }
209
- for (const key in passedProps || {}) {
210
- propsContext[key] = passedProps[key];
211
- }
212
- if (methods && typeof methods === "object") {
213
- for (const [key, value] of Object.entries(methods)) {
214
- renderContext[key] = value.bind(renderContext);
215
- }
216
- }
217
- if (computed && typeof computed === "object") {
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
- }
224
- }
225
- }
226
- return render.call(this, renderContext, ...args);
227
- } : void 0,
228
- setup: (props2) => wrappedSetup(props2, setupContext)
229
- };
230
- return () => h$1(clonedComponent, { ...props, ...setProps, ...attrs }, slots);
231
- }
232
- })
233
- }
234
- )
235
- },
236
- defu(
237
- _options,
238
- {
239
- slots,
240
- attrs,
241
- global: {
242
- config: {
243
- globalProperties: vueApp.config.globalProperties
244
- },
245
- directives: vueApp._context.directives,
246
- provide: vueApp._context.provides,
247
- stubs: {
248
- Suspense: false,
249
- MountSuspendedHelper: false,
250
- [component && typeof component === "object" && "name" in component && typeof component.name === "string" ? component.name : "MountSuspendedComponent"]: false
251
- },
252
- components: { ...vueApp._context.components, RouterLink }
253
- }
254
- }
255
- )
256
- );
257
- }
258
- );
259
- }
260
- function cloneProps$1(props) {
261
- const newProps = reactive({});
262
- for (const key in props) {
263
- newProps[key] = props[key];
264
- }
265
- return newProps;
266
- }
267
- function wrappedMountedWrapper(wrapper) {
268
- const proxy = new Proxy(wrapper, {
269
- get: (target, prop, receiver) => {
270
- if (prop === "element") {
271
- const component = target.findComponent({ name: "MountSuspendedComponent" });
272
- return component[prop];
273
- } else if (prop === "vm") {
274
- const vm = Reflect.get(target, prop, receiver);
275
- return createVMProxy(vm, wrapper.setupState);
276
- } else {
277
- return Reflect.get(target, prop, receiver);
151
+ const SuspendedHelper = {
152
+ name: suspendedHelperName,
153
+ render: () => "",
154
+ async setup() {
155
+ if (route) {
156
+ const router = useRouter();
157
+ await router.replace(route);
278
158
  }
279
- }
280
- });
281
- for (const key of ["props"]) {
282
- proxy[key] = new Proxy(wrapper[key], {
283
- apply: (target, thisArg, args) => {
284
- const component = thisArg.findComponent({ name: "MountSuspendedComponent" });
285
- return component[key](...args);
286
- }
287
- });
288
- }
289
- return proxy;
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
- }
313
-
314
- const WRAPPER_EL_ID = "test-wrapper";
315
- async function renderSuspended(component, options) {
316
- const {
317
- props = {},
318
- attrs = {},
319
- slots = {},
320
- route = "/",
321
- ..._options
322
- } = options || {};
323
- const { render: renderFromTestingLibrary } = await import('@testing-library/vue');
324
- const vueApp = tryUseNuxtApp()?.vueApp || globalThis.__unctx__.get("nuxt-app").tryUse().vueApp;
325
- const { render, setup, data, computed, methods } = component;
326
- let setupContext;
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
- }
345
- for (const fn of window.__cleanup || []) {
346
- fn();
347
- }
348
- document.querySelector(`#${WRAPPER_EL_ID}`)?.remove();
349
- let passedProps;
350
- const wrappedSetup = async (props2, setupContext2) => {
351
- interceptEmitOnCurrentInstance();
352
- passedProps = props2;
353
- if (setup) {
354
- const result = await setup(props2, setupContext2);
355
- setupState = result && typeof result === "object" ? result : {};
356
- return result;
159
+ return () => h$1(ClonedComponent, { ...props, ...setProps, ...attrs }, setupContext.slots);
357
160
  }
358
161
  };
359
- const WrapperComponent = defineComponent$1({
360
- inheritAttrs: false,
361
- render() {
362
- return h$1("div", { id: WRAPPER_EL_ID }, this.$slots.default?.());
363
- }
364
- });
365
- return new Promise((resolve) => {
366
- const utils = renderFromTestingLibrary(
162
+ return new Promise((resolve, reject) => {
163
+ let isMountSettled = false;
164
+ const wrapper = wrapperFn(
367
165
  {
166
+ inheritAttrs: false,
167
+ __cssModules: componentRest.__cssModules,
368
168
  setup: (props2, ctx) => {
169
+ patchInstanceAppContext();
170
+ wrappedInstance = getCurrentInstance();
369
171
  setupContext = ctx;
370
- const scope = effectScope();
371
- window.__cleanup ||= [];
372
- window.__cleanup.push(() => {
373
- scope.stop();
172
+ const nuxtRootSetupResult = runEffectScope(
173
+ () => NuxtRoot.setup(props2, {
174
+ ...ctx,
175
+ expose: () => {
176
+ }
177
+ })
178
+ );
179
+ onErrorCaptured((error, ...args) => {
180
+ if (isMountSettled) return;
181
+ isMountSettled = true;
182
+ try {
183
+ wrappedInstance?.appContext.config.errorHandler?.(error, ...args);
184
+ reject(error);
185
+ } catch (error2) {
186
+ reject(error2);
187
+ }
188
+ return false;
374
189
  });
375
- return scope.run(() => NuxtRoot.setup(props2, {
376
- ...ctx,
377
- expose: () => ({})
378
- }));
190
+ return nuxtRootSetupResult;
379
191
  },
380
- render: (renderContext) => (
381
- // See discussions in https://github.com/testing-library/vue-testing-library/issues/230
382
- // we add this additional root element because otherwise testing-library breaks
383
- // because there's no root element while Suspense is resolving
384
- h$1(
385
- WrapperComponent,
386
- {},
387
- {
388
- default: () => h$1(
389
- Suspense,
390
- {
391
- onResolve: () => nextTick().then(() => {
392
- utils.setupState = setupState;
393
- resolve(utils);
394
- })
395
- },
396
- {
397
- default: () => h$1({
398
- name: "RenderHelper",
399
- async setup() {
400
- const router = useRouter();
401
- await router.replace(route);
402
- const clonedComponent = {
403
- name: "RenderSuspendedComponent",
404
- ...component,
405
- render: render ? function(_ctx, ...args) {
406
- interceptEmitOnCurrentInstance();
407
- if (data && typeof data === "function") {
408
- const dataObject = data();
409
- for (const key in dataObject) {
410
- renderContext[key] = dataObject[key];
411
- }
412
- }
413
- for (const key in setupState || {}) {
414
- const warn = console.warn;
415
- console.warn = () => {
416
- };
417
- try {
418
- renderContext[key] = isReadonly(setupState[key]) ? unref(setupState[key]) : setupState[key];
419
- } catch {
420
- } finally {
421
- console.warn = warn;
422
- }
423
- if (key === "props") {
424
- renderContext[key] = cloneProps(renderContext[key]);
425
- }
426
- }
427
- const propsContext = "props" in renderContext ? renderContext.props : renderContext;
428
- for (const key in props || {}) {
429
- propsContext[key] = _ctx[key];
430
- }
431
- for (const key in passedProps || {}) {
432
- propsContext[key] = passedProps[key];
433
- }
434
- if (methods && typeof methods === "object") {
435
- for (const [key, value] of Object.entries(methods)) {
436
- renderContext[key] = value.bind(renderContext);
437
- }
438
- }
439
- if (computed && typeof computed === "object") {
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
- }
446
- }
447
- }
448
- return render.call(this, renderContext, ...args);
449
- } : void 0,
450
- setup: (props2) => wrappedSetup(props2, setupContext)
451
- };
452
- return () => h$1(clonedComponent, { ...props && typeof props === "object" ? props : {}, ...attrs }, slots);
453
- }
454
- })
192
+ render: wrappedRender(() => h$1(
193
+ Suspense,
194
+ {
195
+ onResolve: () => nextTick$1().then(() => {
196
+ if (isMountSettled) return;
197
+ isMountSettled = true;
198
+ wrapper.setupState = setupState;
199
+ resolve({
200
+ wrapper,
201
+ setProps: (props2) => {
202
+ Object.assign(setProps, props2);
455
203
  }
456
- )
457
- }
458
- )
459
- )
204
+ });
205
+ })
206
+ },
207
+ {
208
+ default: () => h$1(SuspendedHelper)
209
+ }
210
+ ))
460
211
  },
461
- defu(_options, {
462
- slots,
463
- attrs,
212
+ defu(wrapperFnOptions, {
464
213
  global: {
465
214
  config: {
466
- globalProperties: vueApp.config.globalProperties
215
+ globalProperties: makeAllPropertiesEnumerable(
216
+ vueApp.config.globalProperties
217
+ )
467
218
  },
468
219
  directives: vueApp._context.directives,
469
220
  provide: vueApp._context.provides,
470
- components: { RouterLink }
221
+ stubs: {
222
+ Suspense: false,
223
+ [SuspendedHelper.name]: false,
224
+ [ClonedComponent.name]: false
225
+ },
226
+ components: { ...vueApp._context.components, RouterLink }
471
227
  }
472
228
  })
473
229
  );
474
230
  });
475
231
  }
476
- function cloneProps(props) {
477
- const newProps = reactive({});
478
- for (const key in props) {
479
- newProps[key] = props[key];
232
+ function makeAllPropertiesEnumerable(target) {
233
+ return {
234
+ ...target,
235
+ ...Object.fromEntries(
236
+ Object.getOwnPropertyNames(target).map((key) => [key, target[key]])
237
+ )
238
+ };
239
+ }
240
+
241
+ async function mountSuspended(component, options = {}) {
242
+ const suspendedHelperName = "MountSuspendedHelper";
243
+ const clonedComponentName = "MountSuspendedComponent";
244
+ cleanupAll();
245
+ const { wrapper, setProps } = await wrapperSuspended(component, options, {
246
+ wrapperFn: mount,
247
+ suspendedHelperName,
248
+ clonedComponentName
249
+ });
250
+ Object.assign(wrapper, { __setProps: setProps });
251
+ const clonedComponent = wrapper.findComponent({ name: clonedComponentName });
252
+ return wrappedMountedWrapper(wrapper, clonedComponent);
253
+ }
254
+ function wrappedMountedWrapper(wrapper, component) {
255
+ const wrapperProps = [
256
+ "setProps",
257
+ "emitted",
258
+ "setupState",
259
+ "unmount"
260
+ ];
261
+ return new Proxy(wrapper, {
262
+ get: (_, prop, receiver) => {
263
+ if (prop === "getCurrentComponent") return getCurrentComponentPatchedProxy;
264
+ const target = wrapperProps.includes(prop) ? wrapper : Reflect.has(component, prop) ? component : wrapper;
265
+ const value = Reflect.get(target, prop, receiver);
266
+ return typeof value === "function" ? value.bind(target) : value;
267
+ }
268
+ });
269
+ function getCurrentComponentPatchedProxy() {
270
+ const currentComponent = component.getCurrentComponent();
271
+ return new Proxy(currentComponent, {
272
+ get: (target, prop, receiver) => {
273
+ const value = Reflect.get(target, prop, receiver);
274
+ if (prop === "proxy" && value) {
275
+ return new Proxy(value, {
276
+ get(o, p, r) {
277
+ if (!Reflect.has(currentComponent.props, p)) {
278
+ const setupState = wrapper.setupState;
279
+ if (setupState && typeof setupState === "object") {
280
+ if (Reflect.has(setupState, p)) {
281
+ return Reflect.get(setupState, p, r);
282
+ }
283
+ }
284
+ }
285
+ return Reflect.get(o, p, r);
286
+ }
287
+ });
288
+ }
289
+ return value;
290
+ }
291
+ });
480
292
  }
481
- return newProps;
293
+ }
294
+
295
+ async function renderSuspended(component, options = {}) {
296
+ const wrapperId = "test-wrapper";
297
+ const suspendedHelperName = "RenderHelper";
298
+ const clonedComponentName = "RenderSuspendedComponent";
299
+ const { render: wrapperFn } = await import('@testing-library/vue');
300
+ cleanupAll();
301
+ document.getElementById(wrapperId)?.remove();
302
+ const { wrapper, setProps } = await wrapperSuspended(component, options, {
303
+ wrapperFn,
304
+ wrappedRender: (render) => () => h$1({
305
+ inheritAttrs: false,
306
+ render: () => h$1("div", { id: wrapperId }, render())
307
+ }),
308
+ suspendedHelperName,
309
+ clonedComponentName
310
+ });
311
+ wrapper.rerender = async (props) => {
312
+ setProps(props);
313
+ await nextTick();
314
+ };
315
+ return wrapper;
482
316
  }
483
317
 
484
318
  export { mockComponent, mockNuxtImport, mountSuspended, registerEndpoint, renderSuspended };