nuxt / test-utils

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

Using Nuxt lifecycle hooks in component causes unit test to timeout #883

Open laura-richer opened 3 weeks ago

laura-richer commented 3 weeks ago

Environment

Reproduction

https://stackblitz.com/edit/github-kd86ew?file=app.nuxt.test.ts

Command: npm run test

Describe the bug

I am using Nuxt's page:start lifecycle hook to trigger a ref boolean change

When using the lifecycle hook/defining const nuxtApp = useNuxtApp() in my component, a unit test that mounts this component times out.

This happens regardless of which lifecycle hook is used (page:finish, page:loading:start, app:created).

Results in:

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  app.nuxt.test.ts > App > calls page:start hook
Error: Test timed out in 5000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
 ❯ _0x44f3c3 ../../../blitz.70dbe416.js:40:517443
 ❯ ../../../blitz.70dbe416.js:40:517346

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

Vitest caught 1 unhandled error during the test run.
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
TypeError: 'set' on proxy: trap returned falsish for property 'nuxtApp'
 ❯ clonedComponent.render node_modules/@nuxt/test-utils/dist/runtime-utils/index.mjs:134:44
    132|                       return render.call(this, renderContext, ...args);
    133|                     } : void 0,
    134|                     setup: setup ? (props2) => wrappedSetup(props2, setupContext) : void 0
       |                                            ^
    135|                   };
    136|                   return () => h$1(clonedComponent, { ...defu(setProps, props), ...attrs }, slots);
 ❯ renderComponentRoot node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:886:16
 ❯ ReactiveEffect.componentUpdateFn node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5080:46
 ❯ ReactiveEffect.run node_modules/@vue/reactivity/dist/reactivity.cjs.js:181:19
 ❯ instance.update node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5211:16
 ❯ setupRenderEffect node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5221:5
 ❯ eval node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:1639:9

This error originated in "app.nuxt.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "calls page:start hook". It might mean one of the following:
- The error was thrown, while Vitest was running this test.
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 Test Files  1 failed (1)
      Tests  1 failed (1)

Additional context

No response

Logs

No response

dosubot[bot] commented 3 weeks ago

@laura-richer Hello there! I'm here to help you with any issues or questions you have. If you need assistance with bug fixes, answers to technical queries, or want to become a contributor, I'm here for you.

To resolve the issue of Nuxt lifecycle hooks causing your unit tests to timeout, you can mock the lifecycle hooks or the asynchronous operations within them. This will prevent the actual delay from occurring during your tests. Here is an example of how you can mock the setTimeout function in your tests:

jest.useFakeTimers();

test('Nuxt plugin test', async () => {
  const plugin = require('path/to/your/plugin').default;

  // Mock the setTimeout function
  jest.spyOn(global, 'setTimeout').mockImplementation((cb) => cb());

  await plugin();

  // Restore the original setTimeout function
  jest.useRealTimers();
});

In this example, jest.useFakeTimers() is used to mock the timer functions, and jest.spyOn(global, 'setTimeout').mockImplementation((cb) => cb()) is used to immediately invoke the callback passed to setTimeout, effectively bypassing the delay. After the test, jest.useRealTimers() is called to restore the original timer functions.

For instance, if you have a Nuxt plugin like this:

export default defineNuxtPlugin(async (_nuxtApp) => {
  await new Promise((resolve) => {
    setTimeout(resolve, 1000)
  })

  return {
    provide: {
      async: {
        didInject: true,
      },
    },
  }
})

You can apply the same mocking strategy to ensure your tests do not timeout due to the setTimeout delay.

Additionally, you can refer to the Nuxt composables tests for more examples of handling asynchronous operations and lifecycle hooks in tests. For example, the useAsyncData composable is tested with various scenarios including handling errors, immediate execution, and custom cache access:

describe('useAsyncData', () => {
  it('should work at basic level', async () => {
    const res = useAsyncData(() => Promise.resolve('test'))
    expect(Object.keys(res)).toMatchInlineSnapshot(`
      [
        "data",
        "pending",
        "error",
        "status",
        "execute",
        "refresh",
        "clear",
      ]
    `)
    expect(res instanceof Promise).toBeTruthy()
    expect(res.data.value).toBe(undefined)
    await res
    expect(res.data.value).toBe('test')
  })

  it('should capture errors', async () => {
    const { data, error, status, pending } = await useAsyncData('error-test', () => Promise.reject(new Error('test')), { default: () => 'default' })
    expect(data.value).toMatchInlineSnapshot('"default"')
    expect(error.value).toMatchInlineSnapshot('[Error: test]')
    expect(status.value).toBe('error')
    expect(pending.value).toBe(false)
  })

  it('should initialize with error set to null when immediate: false', async () => {
    const { error, execute } = useAsyncData(() => Promise.resolve({}), { immediate: false })
    expect(error.value).toBe(undefined)
    await execute()
    expect(error.value).toBe(undefined)
  })
})

These examples demonstrate how to handle asynchronous operations and ensure your tests do not timeout [1][2][3].

To continue talking to Dosu, mention @dosu.