nuxt / test-utils

🧪 Test utilities for Nuxt
http://nuxt.com/docs/getting-started/testing
MIT License
310 stars 79 forks source link

registerEndpoint does not expose endpoint to $fetch from @nuxt/test-utils (ofetch) #943

Open eriksLapins opened 5 days ago

eriksLapins commented 5 days ago

Environment


Reproduction

A minimal repro showing that nitro's (I think) $fetch works while the one from test-utils module (ofetch/happy-dom) does not

Describe the bug

Using $fetch as in the examples from the documentation will not be able to find registered server routes in the tests. The problem is that in case you want to render component/page in the tests, it will use $fetch from happy-dom which cannot find these registered routes.

Additional context

No response

Logs

> nuxt-app@ test C:\Users\User\Desktop\projects\testing-testing
> vitest

 DEV  v2.0.5 C:/Users/User/Desktop/projects/testing-testing

stdout | tests/app.nuxt.test.ts
<Suspense> is an experimental feature and its API will likely change.

stdout | tests/app.nuxt.test.ts > App 2 > fetch that works with registerEndpoint
fetch response { data: { some: 'test' } }

 ❯ tests/app.nuxt.test.ts (2)
   ❯ App (1)
     × fetch that doesn't work with registerEndpoint
   ✓ App 2 (1)
     ✓ fetch that works with registerEndpoint

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
 FAIL  tests/app.nuxt.test.ts > App > fetch that doesn't work with registerEndpoint
FetchError: [GET] "/some/test/endpoint": <no response> Failed to parse URL from /some/test/endpoint
 ❯ $fetchRaw2 node_modules/.pnpm/ofetch@1.3.4/node_modules/ofetch/dist/shared/ofetch.37386b05.mjs:231:14
 ❯ Module.$fetch2 node_modules/.pnpm/ofetch@1.3.4/node_modules/ofetch/dist/shared/ofetch.37386b05.mjs:268:15
 ❯ tests/app.nuxt.test.ts:17:26
     15| describe('App', async () => {
     16|     it('fetch that doesn\'t work with registerEndpoint', async () => {
     17|         const response = await $utilsFetch('/some/test/endpoint');
       |                          ^
     18|         console.log('utils response', response);
     19|         expect(JSON.stringify(response)).toBe(JSON.stringify({data: {

Caused by: TypeError: Failed to parse URL from /some/test/endpoint
 ❯ $fetchRaw2 node_modules/.pnpm/ofetch@1.3.4/node_modules/ofetch/dist/shared/ofetch.37386b05.mjs:222:26

Caused by: TypeError: Invalid URL
 ❯ new URL node_modules/.pnpm/happy-dom@15.7.4/node_modules/happy-dom/src/url/URL.ts:9:15
 ❯ $fetchRaw2 node_modules/.pnpm/ofetch@1.3.4/node_modules/ofetch/dist/shared/ofetch.37386b05.mjs:222:32
 ❯ $fetchRaw2 node_modules/.pnpm/ofetch@1.3.4/node_modules/ofetch/dist/shared/ofetch.37386b05.mjs:231:20

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯Serialized Error: { code: 'ERR_INVALID_URL', input: '/some/test/endpoint' }
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯

 Test Files  1 failed (1)
      Tests  1 failed | 1 passed (2)
   Start at  16:25:43
   Duration  3.10s (transform 900ms, setup 725ms, collect 1.12s, tests 16ms, environment 493ms, prepare 464ms)

 FAIL  Tests failed. Watching for file changes...
       press h to show help, press q to quit
Cancelling test run. Press CTRL+c again to exit forcefully.

 ELIFECYCLE  Test failed. See above for more details.
dosubot[bot] commented 5 days ago

Hey @eriksLapins! 👋 I'm here to help you with your software bugs, questions, and becoming a contributor.

The issue you're encountering with registerEndpoint not exposing the endpoint to $fetch from @nuxt/test-utils (ofetch) in your Nuxt 3.13.1 environment is due to the fact that @nuxt/test-utils/runtime and @nuxt/test-utils/e2e need to run in different testing environments and cannot be used in the same file.

To resolve this, you should split your tests into separate files and specify the appropriate test environment for each file. Here’s how you can do it:

  1. Unit Test File (app.nuxt.spec.ts):

    import { registerEndpoint } from '@nuxt/test-utils/runtime'
    
    registerEndpoint('/test/', () => ({
     test: 'test-field'
    }))
    
    // Your unit tests here
  2. End-to-End Test File (app.e2e.spec.ts):

    import { setup, $fetch } from '@nuxt/test-utils/e2e'
    
    await setup({
     setupTimeout: 10000,
    })
    
    // Your end-to-end tests here

This separation ensures that the registerEndpoint functionality is correctly utilized within the appropriate testing environment [1][2].

Additionally, it's important to understand how $fetch works in Nuxt. $fetch is a utility provided by the ofetch library and is auto-imported globally across your application. It is used behind the scenes by useFetch and useAsyncData composables to perform data fetching. However, using $fetch alone does not provide network calls de-duplication and navigation prevention, which are essential for efficient data fetching in Nuxt applications. Therefore, it is recommended to use $fetch for client-side interactions or in combination with useAsyncData for fetching initial component data [2].

For more details, you can refer to the Nuxt documentation on testing [1].

To continue talking to Dosu, mention @dosu.

eriksLapins commented 5 days ago

By the suggestion of @dosu I split into two files and added three tests in each, but now it cannot even setup, it just times out after 10 seconds because it cannot setup. If I just add createContext() at least it will setup, hovewer, it still cannot get the mocked response. For the index page I found that useAsyncData will return an empty response (no data to be tested for the app) and onMounted the $fetch will turn out in an error.

dosubot[bot] commented 5 days ago

To address the setup timeout issue and ensure that mocked responses are correctly fetched using useAsyncData and $fetch, you can follow these steps:

  1. Increase the Setup Timeout: Ensure that the setup timeout is sufficient for your environment. You can increase the setupTimeout in your setup function.

  2. Ensure Proper Context Initialization: Make sure that the test context is properly initialized and exposed to the environment.

  3. Mock Endpoints Correctly: Ensure that the endpoints are correctly registered and accessible during the tests.

Here’s an example of how you can set up your tests:

Unit Test File (app.nuxt.spec.ts):

import { registerEndpoint } from '@nuxt/test-utils/runtime'
import { defineEventHandler } from 'h3'

registerEndpoint('/test/', defineEventHandler(() => ({
  test: 'test-field'
})))

registerEndpoint('/api/hello', defineEventHandler(() => ({
  message: 'Hello World'
})))

registerEndpoint('/api/hey', defineEventHandler(() => ({
  foo: 'bar',
  baz: 'qux'
})))

// Your unit tests here

End-to-End Test File (app.e2e.spec.ts):

import { setup, $fetch } from '@nuxt/test-utils/e2e'

await setup({
  setupTimeout: 30000, // Increase the setup timeout
})

describe('App', () => {
  it('fetches data from /test/ endpoint', async () => {
    const response = await $fetch('/test/')
    expect(response).toEqual({ test: 'test-field' })
  })

  it('fetches data from /api/hello endpoint', async () => {
    const { data } = await useAsyncData('api-hello', () => $fetch('/api/hello'))
    expect(data.value).toEqual({ message: 'Hello World' })
  })

  it('fetches data from /api/hey endpoint', async () => {
    const { data } = await useAsyncData('api-hey', () => $fetch('/api/hey'))
    expect(data.value).toEqual({ foo: 'bar', baz: 'qux' })
  })
})

Handling Timeout in useAsyncData and $fetch:

Ensure that the timeout is correctly handled and the request is aborted if it exceeds the specified timeout duration:

const timeoutLength = toValue(opts.timeout)
let timeoutId: NodeJS.Timeout
if (timeoutLength) {
  timeoutId = setTimeout(() => controller.abort('Request aborted due to timeout.'), timeoutLength)
  controller.signal.onabort = () => clearTimeout(timeoutId)
}

let _$fetch = opts.$fetch || globalThis.$fetch

if (import.meta.server && !opts.$fetch) {
  const isLocalFetch = typeof _request.value === 'string' && _request.value[0] === '/' && (!toValue(opts.baseURL) || toValue(opts.baseURL)![0] === '/')
  if (isLocalFetch) {
    _$fetch = useRequestFetch()
  }
}

return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any).finally(() => { clearTimeout(timeoutId) }) as Promise<_ResT>

Using Composables for Testing:

You can use composables like useSleep and useCounter to simulate async data fetching and ensure that the mocked responses are correctly fetched:

export const useSleep = () => useAsyncData('sleep', async () => {
  await new Promise(resolve => setTimeout(resolve, 50))
  return 'Slept!'
})

export const useCounter = () => useFetch('/api/useAsyncData/count')

These steps should help resolve the setup timeout issue and ensure that mocked responses are correctly fetched using useAsyncData and $fetch in your tests [1][2][3].

To continue talking to Dosu, mention @dosu.